From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Martijn Muijsers Date: Fri, 2 Dec 2022 11:43:51 +0100 Subject: [PATCH] Base thread pool License: AGPL-3.0 (https://www.gnu.org/licenses/agpl-3.0.html) Gale - https://galemc.org diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java index b71404be2c82f7db35272b367af861e94d6c73d3..22089204d13366bb265305ef14d08b0468ff5055 100644 --- a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java +++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java @@ -1,10 +1,15 @@ package ca.spottedleaf.concurrentutil.executor.standard; +import net.minecraft.server.MinecraftServer; +import org.galemc.gale.executor.queue.AllLevelsScheduledMainThreadChunkTaskQueue; + import java.util.ArrayDeque; import java.util.concurrent.atomic.AtomicLong; public class PrioritisedThreadedTaskQueue implements PrioritisedExecutor { + private final boolean influenceMayHaveDelayedTasks; // Gale - base thread pool + protected final ArrayDeque[] queues = new ArrayDeque[Priority.TOTAL_SCHEDULABLE_PRIORITIES]; { for (int i = 0; i < Priority.TOTAL_SCHEDULABLE_PRIORITIES; ++i) { this.queues[i] = new ArrayDeque<>(); @@ -20,6 +25,16 @@ public class PrioritisedThreadedTaskQueue implements PrioritisedExecutor { protected long taskIdGenerator = 0; + public PrioritisedThreadedTaskQueue() { + this(false); + } + + // Gale start - base thread pool + public PrioritisedThreadedTaskQueue(boolean influenceMayHaveDelayedTasks) { + this.influenceMayHaveDelayedTasks = influenceMayHaveDelayedTasks; + } + // Gale end - base thread pool + @Override public PrioritisedExecutor.PrioritisedTask queueRunnable(final Runnable task, final PrioritisedExecutor.Priority priority) throws IllegalStateException, IllegalArgumentException { if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { @@ -145,6 +160,12 @@ public class PrioritisedThreadedTaskQueue implements PrioritisedExecutor { } protected final long getAndAddTotalScheduledTasksVolatile(final long value) { + // Gale start - base thread pool + if (this.influenceMayHaveDelayedTasks) { + MinecraftServer.nextTimeAssumeWeMayHaveDelayedTasks = true; + AllLevelsScheduledMainThreadChunkTaskQueue.signalReason.signalAnother(); + } + // Gale end - base thread pool return this.totalScheduledTasks.getAndAdd(value); } @@ -158,6 +179,12 @@ public class PrioritisedThreadedTaskQueue implements PrioritisedExecutor { return this.totalCompletedTasks.getAndAdd(value); } + // Gale start - base thread pool + public final boolean hasScheduledUncompletedTasksVolatile() { + return this.totalScheduledTasks.get() > this.totalCompletedTasks.get(); + } + // Gale end - base thread pool + protected static final class PrioritisedTask implements PrioritisedExecutor.PrioritisedTask { protected final PrioritisedThreadedTaskQueue queue; protected long id; diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java index dabd93c35bdbac6a8b668a82d5f3d4173a1baa4a..f22e8930e780b4bf11052504c996b8852d0c5304 100644 --- a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java +++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java @@ -21,6 +21,7 @@ import net.minecraft.world.level.block.EntityBlock; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.chunk.*; import org.bukkit.Bukkit; +import org.galemc.gale.executor.queue.ScheduledMainThreadTaskQueues; import java.util.*; import java.util.concurrent.Executor; @@ -180,7 +181,7 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo if (!Bukkit.isPrimaryThread()) { // Plugins? - MinecraftServer.getServer().scheduleOnMain(() -> modifyBlocks(chunkPacket, chunkPacketInfo)); + ScheduledMainThreadTaskQueues.add(() -> modifyBlocks(chunkPacket, chunkPacketInfo), ScheduledMainThreadTaskQueues.ANTI_XRAY_MODIFY_BLOCKS_TASK_MAX_DELAY); // Gale - base thread pool return; } diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java index 84cc9397237fa0c17aa1012dfb5683c90eb6d3b8..d89b8ea24844185661ac93dd4cc9f26696e967cb 100644 --- a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java +++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java @@ -113,7 +113,7 @@ public final class ChunkTaskScheduler { public final PrioritisedThreadPool.PrioritisedPoolExecutor parallelGenExecutor; public final PrioritisedThreadPool.PrioritisedPoolExecutor loadExecutor; - private final PrioritisedThreadedTaskQueue mainThreadExecutor = new PrioritisedThreadedTaskQueue(); + public final PrioritisedThreadedTaskQueue mainThreadExecutor = new PrioritisedThreadedTaskQueue(true); // Gale end - base thread pool - private -> public, count delayed tasks final ReentrantLock schedulingLock = new ReentrantLock(); public final ChunkHolderManager chunkHolderManager; diff --git a/src/main/java/io/papermc/paper/configuration/Configurations.java b/src/main/java/io/papermc/paper/configuration/Configurations.java index cf6d50218769e3fecd12dbde70a03b5042feddf4..9d8ee965f7dcd0f416b7aa8368e34b911edef6b0 100644 --- a/src/main/java/io/papermc/paper/configuration/Configurations.java +++ b/src/main/java/io/papermc/paper/configuration/Configurations.java @@ -322,7 +322,7 @@ public abstract class Configurations { YamlConfiguration global = YamlConfiguration.loadConfiguration(this.globalFolder.resolve(this.globalConfigFileName).toFile()); ConfigurationSection worlds = global.createSection(legacyWorldsSectionKey); worlds.set(legacyWorldDefaultsSectionKey, YamlConfiguration.loadConfiguration(this.globalFolder.resolve(this.defaultWorldConfigFileName).toFile())); - for (ServerLevel level : server.getAllLevels()) { + for (ServerLevel level : server.getAllLevelsArray()) { // Gale - base thread pool - optimize server levels worlds.set(level.getWorld().getName(), YamlConfiguration.loadConfiguration(getWorldConfigFile(level).toFile())); } return global; diff --git a/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java b/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java index ceef4eed87363298816426dfc19f3207be1af682..ec76ed64fe3082810f4d790c5b73e731d5d524e9 100644 --- a/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java +++ b/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java @@ -285,7 +285,7 @@ public class PaperConfigurations extends Configurations(), - new ThreadFactoryBuilder() - .setNameFormat("Paper Async Task Handler Thread - %1$d") - .setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(MinecraftServer.LOGGER)) - .build() - ); - public static final ThreadPoolExecutor cleanerExecutor = new ThreadPoolExecutor( - 1, 1, 0L, TimeUnit.SECONDS, - new LinkedBlockingQueue<>(), - new ThreadFactoryBuilder() - .setNameFormat("Paper Object Cleaner") - .setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(MinecraftServer.LOGGER)) - .build() - ); + public static final Executor asyncExecutor = BaseTaskQueues.scheduledAsync.yieldingExecutor; // Gale - base thread pool - remove Paper async executor + public static final Executor cleanerExecutor = BaseTaskQueues.scheduledAsync.yieldingExecutor; // Gale - base thread pool - remove Paper cleaner executor public static final long INVALID_CHUNK_KEY = getCoordinateKey(Integer.MAX_VALUE, Integer.MAX_VALUE); diff --git a/src/main/java/io/papermc/paper/util/TickThread.java b/src/main/java/io/papermc/paper/util/TickThread.java index fc57850b80303fcade89ca95794f63910404a407..8f95f98eef90d2618ffc051885bfbd3dc121f561 100644 --- a/src/main/java/io/papermc/paper/util/TickThread.java +++ b/src/main/java/io/papermc/paper/util/TickThread.java @@ -4,9 +4,12 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.entity.Entity; import org.bukkit.Bukkit; +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.MainThreadClaim; + import java.util.concurrent.atomic.AtomicInteger; -public class TickThread extends Thread { +public class TickThread extends BaseThread { // Gale - base thread pool public static final boolean STRICT_THREAD_CHECKS = Boolean.getBoolean("paper.strict-thread-checks"); @@ -65,23 +68,19 @@ public class TickThread extends Thread { } private TickThread(final Runnable run, final String name, final int id) { - super(run, name); + super(run, name, 0); // Gale - base thread pool this.id = id; } - public static TickThread getCurrentTickThread() { - return (TickThread)Thread.currentThread(); - } - public static boolean isTickThread() { - return Thread.currentThread() instanceof TickThread; + return MainThreadClaim.isCurrentThreadMainThreadAndNotClaimable(); // Gale - base thread pool } public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ) { - return Thread.currentThread() instanceof TickThread; + return MainThreadClaim.isCurrentThreadMainThreadAndNotClaimable(); // Gale - base thread pool } public static boolean isTickThreadFor(final Entity entity) { - return Thread.currentThread() instanceof TickThread; + return MainThreadClaim.isCurrentThreadMainThreadAndNotClaimable(); // Gale - base thread pool } } diff --git a/src/main/java/io/papermc/paper/world/ThreadedWorldUpgrader.java b/src/main/java/io/papermc/paper/world/ThreadedWorldUpgrader.java index 95cac7edae8ac64811fc6a2f6b97dd4a0fceb0b0..a376259202b4a16c67db4d3ef071e0b395aca524 100644 --- a/src/main/java/io/papermc/paper/world/ThreadedWorldUpgrader.java +++ b/src/main/java/io/papermc/paper/world/ThreadedWorldUpgrader.java @@ -7,25 +7,21 @@ import net.minecraft.nbt.CompoundTag; import net.minecraft.resources.ResourceKey; import net.minecraft.util.worldupdate.WorldUpgrader; import net.minecraft.world.level.ChunkPos; -import net.minecraft.world.level.Level; import net.minecraft.world.level.chunk.ChunkGenerator; import net.minecraft.world.level.chunk.storage.ChunkStorage; import net.minecraft.world.level.chunk.storage.RegionFileStorage; -import net.minecraft.world.level.dimension.DimensionType; import net.minecraft.world.level.dimension.LevelStem; -import net.minecraft.world.level.levelgen.WorldGenSettings; import net.minecraft.world.level.storage.DimensionDataStorage; import net.minecraft.world.level.storage.LevelStorageSource; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.galemc.gale.executor.queue.BaseTaskQueues; + import java.io.File; import java.io.IOException; import java.text.DecimalFormat; import java.util.Optional; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Supplier; @@ -46,6 +42,10 @@ public class ThreadedWorldUpgrader { this.dimensionType = dimensionType; this.worldName = worldName; this.worldDir = worldDir; + // Gale start - base thread pool - remove world upgrade executors + this.threadPool = BaseTaskQueues.scheduledAsync.yieldingExecutor; + /* + // Gale end - base thread pool - remove world upgrade executors this.threadPool = Executors.newFixedThreadPool(Math.max(1, threads), new ThreadFactory() { private final AtomicInteger threadCounter = new AtomicInteger(); @@ -61,6 +61,7 @@ public class ThreadedWorldUpgrader { return ret; } }); + */ // Gale - base thread pool - remove world upgrade executors this.dataFixer = dataFixer; this.generatorKey = generatorKey; this.removeCaches = removeCaches; diff --git a/src/main/java/me/titaniumtown/ArrayConstants.java b/src/main/java/me/titaniumtown/ArrayConstants.java index ceec23a85aae625fbbe2db95c8e9c83fb9f9767c..fdb9eefdcbd4abf1936761136077c3d10ef5e594 100644 --- a/src/main/java/me/titaniumtown/ArrayConstants.java +++ b/src/main/java/me/titaniumtown/ArrayConstants.java @@ -2,6 +2,8 @@ package me.titaniumtown; +import net.minecraft.server.level.ServerLevel; + public final class ArrayConstants { private ArrayConstants() {} @@ -14,5 +16,6 @@ public final class ArrayConstants { public static final long[] emptyLongArray = new long[0]; public static final org.bukkit.entity.Entity[] emptyBukkitEntityArray = new org.bukkit.entity.Entity[0]; public static final net.minecraft.world.entity.Entity[] emptyEntityArray = new net.minecraft.world.entity.Entity[0]; + public static final ServerLevel[] emptyServerLevelArray = new ServerLevel[0]; // Gale - base thread pool } diff --git a/src/main/java/net/minecraft/Util.java b/src/main/java/net/minecraft/Util.java index 6b7943e8348b0a41ca69fb56ccfd5f1c1484eb07..c2cab5f6be64e7a7adf03bb004709d81cf0eee42 100644 --- a/src/main/java/net/minecraft/Util.java +++ b/src/main/java/net/minecraft/Util.java @@ -27,9 +27,6 @@ import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.spi.FileSystemProvider; -import java.security.AccessController; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; import java.time.Duration; import java.time.Instant; import java.time.ZonedDateTime; @@ -47,8 +44,6 @@ import java.util.concurrent.CompletionException; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.ForkJoinPool; -import java.util.concurrent.ForkJoinWorkerThread; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -67,11 +62,11 @@ import java.util.stream.Stream; import javax.annotation.Nullable; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.Bootstrap; -import net.minecraft.util.Mth; import net.minecraft.util.RandomSource; import net.minecraft.util.TimeSource; import net.minecraft.util.datafix.DataFixers; import net.minecraft.world.level.block.state.properties.Property; +import org.galemc.gale.executor.queue.BaseTaskQueues; import org.slf4j.Logger; public class Util { @@ -79,8 +74,8 @@ public class Util { private static final int DEFAULT_MAX_THREADS = 255; private static final String MAX_THREADS_SYSTEM_PROPERTY = "max.bg.threads"; private static final AtomicInteger WORKER_COUNT = new AtomicInteger(1); - private static final ExecutorService BOOTSTRAP_EXECUTOR = makeExecutor("Bootstrap", -2); // Paper - add -2 priority - private static final ExecutorService BACKGROUND_EXECUTOR = makeExecutor("Main", -1); // Paper - add -1 priority + private static final ExecutorService BACKGROUND_EXECUTOR = BaseTaskQueues.scheduledAsync.yieldingExecutor; // Gale - base thread pool - remove background executor + private static final ExecutorService BOOTSTRAP_EXECUTOR = BACKGROUND_EXECUTOR; // Gale - Patina - remove bootstrap executor // Paper start - don't submit BLOCKING PROFILE LOOKUPS to the world gen thread public static final ExecutorService PROFILE_EXECUTOR = Executors.newFixedThreadPool(2, new java.util.concurrent.ThreadFactory() { @@ -219,7 +214,6 @@ public class Util { } public static void shutdownExecutors() { - shutdownExecutor(BACKGROUND_EXECUTOR); shutdownExecutor(IO_POOL); } diff --git a/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java b/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java index 40812e6518b8aacfbd2d8cd65a407d00bb19e991..eb1209b760890e6ede8ee149a6debfe83336ee10 100644 --- a/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java +++ b/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java @@ -144,10 +144,7 @@ public class EntitySelector { if (this.isWorldLimited()) { this.addEntities(list, source.getLevel(), vec3d, predicate); } else { - Iterator iterator1 = source.getServer().getAllLevels().iterator(); - - while (iterator1.hasNext()) { - ServerLevel worldserver1 = (ServerLevel) iterator1.next(); + for (ServerLevel worldserver1 : source.getServer().getAllLevelsArray()) { // Gale - base thread pool - optimize server levels this.addEntities(list, worldserver1, vec3d, predicate); } diff --git a/src/main/java/net/minecraft/network/protocol/PacketUtils.java b/src/main/java/net/minecraft/network/protocol/PacketUtils.java index 8bc0cb9ad5bb4e76d962ff54305e2c08e279a17b..405ffeb584d60b3677ae5d8fb79e084635d2dea3 100644 --- a/src/main/java/net/minecraft/network/protocol/PacketUtils.java +++ b/src/main/java/net/minecraft/network/protocol/PacketUtils.java @@ -2,6 +2,7 @@ package net.minecraft.network.protocol; import com.mojang.logging.LogUtils; import net.minecraft.network.PacketListener; +import org.galemc.gale.executor.AbstractBlockableEventLoop; import org.slf4j.Logger; // CraftBukkit start @@ -9,7 +10,6 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.server.RunningOnDifferentThreadException; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.network.ServerGamePacketListenerImpl; -import net.minecraft.util.thread.BlockableEventLoop; public class PacketUtils { @@ -36,10 +36,10 @@ public class PacketUtils { public PacketUtils() {} public static void ensureRunningOnSameThread(Packet packet, T listener, ServerLevel world) throws RunningOnDifferentThreadException { - PacketUtils.ensureRunningOnSameThread(packet, listener, (BlockableEventLoop) world.getServer()); + PacketUtils.ensureRunningOnSameThread(packet, listener, world.getServer()); // Gale - base thread pool } - public static void ensureRunningOnSameThread(Packet packet, T listener, BlockableEventLoop engine) throws RunningOnDifferentThreadException { + public static void ensureRunningOnSameThread(Packet packet, T listener, AbstractBlockableEventLoop engine) throws RunningOnDifferentThreadException { // Gale - base thread pool if (!engine.isSameThread()) { engine.executeIfPossible(() -> { packetProcessing.push(listener); // Paper - detailed watchdog information diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java index d8f8d2495c1e2e3f194485d16ea587d26cc3a23d..ae423cc7ef2d19d7d5f89ac9cb2962055acda40e 100644 --- a/src/main/java/net/minecraft/server/Main.java +++ b/src/main/java/net/minecraft/server/Main.java @@ -55,6 +55,7 @@ import net.minecraft.world.level.levelgen.presets.WorldPresets; import net.minecraft.world.level.storage.LevelResource; import net.minecraft.world.level.storage.LevelStorageSource; import net.minecraft.world.level.storage.LevelSummary; +import org.galemc.gale.executor.thread.SecondaryThreadPool; import org.slf4j.Logger; // CraftBukkit start @@ -224,6 +225,8 @@ public class Main { AtomicReference> ops = new AtomicReference<>(); // CraftBukkit end + SecondaryThreadPool.startSecondaryThreads(); // Gale - base thread pool + WorldStem worldstem; try { diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java index 0b3b1400e9546060b4ee35236741670aaa226820..8c9738bf077ff37aae269959d071f640e2b1deab 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -10,6 +10,7 @@ import com.mojang.authlib.GameProfileRepository; import com.mojang.authlib.minecraft.MinecraftSessionService; import com.mojang.datafixers.DataFixer; import com.mojang.logging.LogUtils; +import io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler; import it.unimi.dsi.fastutil.longs.LongIterator; import java.awt.image.BufferedImage; import java.io.BufferedWriter; @@ -40,7 +41,7 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; -import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BooleanSupplier; import java.util.function.Consumer; @@ -115,7 +116,6 @@ import net.minecraft.util.profiling.metrics.profiling.InactiveMetricsRecorder; import net.minecraft.util.profiling.metrics.profiling.MetricsRecorder; import net.minecraft.util.profiling.metrics.profiling.ServerMetricsSamplersProvider; import net.minecraft.util.profiling.metrics.storage.MetricsPersister; -import net.minecraft.util.thread.ReentrantBlockableEventLoop; import net.minecraft.world.Difficulty; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.ai.village.VillageSiege; @@ -150,8 +150,17 @@ import net.minecraft.world.level.storage.loot.PredicateManager; import net.minecraft.world.phys.Vec2; import net.minecraft.world.phys.Vec3; import org.apache.commons.lang3.Validate; +import org.bukkit.Bukkit; +import org.galemc.gale.executor.MinecraftServerBlockableEventLoop; import org.galemc.gale.configuration.GaleConfigurations; import org.galemc.gale.configuration.GaleGlobalConfiguration; +import org.galemc.gale.executor.queue.BaseTaskQueues; +import org.galemc.gale.executor.queue.ScheduledMainThreadTaskQueues; +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.MainThreadClaim; +import org.galemc.gale.executor.thread.SecondaryThreadPool; +import org.galemc.gale.executor.thread.wait.SignalReason; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; // CraftBukkit start @@ -174,9 +183,12 @@ import org.bukkit.event.server.ServerLoadEvent; import co.aikar.timings.MinecraftTimings; // Paper -public abstract class MinecraftServer extends ReentrantBlockableEventLoop implements CommandSource, AutoCloseable { +public abstract class MinecraftServer extends MinecraftServerBlockableEventLoop implements CommandSource, AutoCloseable { // Gale - base thread pool - private static MinecraftServer SERVER; // Paper + // Gale start - base thread pool + public static MinecraftServer SERVER; // Paper // Gale - base thread pool - private -> public + public static boolean canPollAsyncTasksOnOriginalServerThread = true; + // Gale end - base thread pool public static final Logger LOGGER = LogUtils.getLogger(); public static final String VANILLA_BRAND = "vanilla"; private static final float AVERAGE_TICK_TIME_SMOOTHING = 0.8F; @@ -214,6 +226,10 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop, ServerLevel> levels; + // Gale start - base thread pool - optimize server levels + private @NotNull ServerLevel @NotNull [] levelArray = ArrayConstants.emptyServerLevelArray; + private @Nullable ServerLevel overworld; + // Gale end - base thread pool - optimize server levels private PlayerList playerList; private volatile boolean running; private volatile boolean isRestarting = false; // Paper - flag to signify we're attempting to restart @@ -243,10 +259,52 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop processQueue = new java.util.concurrent.ConcurrentLinkedQueue(); public int autosavePeriod; public Commands vanillaCommandDispatcher; - public boolean forceTicks; // Paper + public volatile boolean forceTicks; // Paper // Gale - base thread pool - make fields volatile // CraftBukkit end // Spigot start public static final int TPS = 20; @@ -292,9 +350,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop S spin(Function serverFactory) { + public static S spin(Function serverFactory) { // Gale - base thread pool AtomicReference atomicreference = new AtomicReference(); - Thread thread = new io.papermc.paper.util.TickThread(() -> { // Paper - rewrite chunk system + BaseThread thread = new io.papermc.paper.util.TickThread(() -> { // Paper - rewrite chunk system // Gale - base thread pool ((MinecraftServer) atomicreference.get()).runServer(); }, "Server thread"); @@ -313,9 +371,10 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop registryreadops, Thread thread, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PackRepository resourcepackrepository, WorldStem worldstem, Proxy proxy, DataFixer datafixer, Services services, ChunkProgressListenerFactory worldloadlistenerfactory) { - super("Server"); + public MinecraftServer(OptionSet options, DataPackConfig datapackconfiguration, DynamicOps registryreadops, BaseThread thread, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PackRepository resourcepackrepository, WorldStem worldstem, Proxy proxy, DataFixer datafixer, Services services, ChunkProgressListenerFactory worldloadlistenerfactory) { // Gale - base thread pool + super(); // Gale - base thread pool SERVER = this; // Paper - better singleton + canPollAsyncTasksOnOriginalServerThread = false; this.metricsRecorder = InactiveMetricsRecorder.INSTANCE; this.profiler = this.metricsRecorder.getProfiler(); this.onMetricsRecordingStopped = (methodprofilerresults) -> { @@ -353,7 +412,11 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop public public volatile boolean hasFullyShutdown = false; // Paper private boolean hasLoggedStop = false; // Paper private final Object stopLock = new Object(); @@ -922,8 +981,10 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop !BaseTaskQueues.scheduledAsync.hasTasks(), null, System.nanoTime() + 30_000_000_000L); // Paper + LOGGER.info("Shutting down IO executor..."); + // Gale end - base thread pool - remove Paper async executor + // Gale end - base thread pool - remove background executor Util.shutdownExecutors(); // Paper LOGGER.info("Closing Server"); try { @@ -1026,7 +1087,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop public // Paper start if (this.forceTicks) { return true; @@ -1273,13 +1373,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { return !this.canSleepForTickNoOversleep(); // Paper - move oversleep into full server tick - }); + // Gale start - base thread pool + }, 1_000_000L * this.nextTickTime - 200_000L); + isInSpareTime = false; + // Gale end - base thread pool lastTickOversleepTime = (System.nanoTime() - tickOversleepStart) / 1000000L; // Gale - YAPFA - last tick time } - @Override - public TickTask wrapRunnable(Runnable runnable) { - // Paper start - anything that does try to post to main during watchdog crash, run on watchdog - if (this.hasStopped && Thread.currentThread().equals(shutdownThread)) { - runnable.run(); - runnable = () -> {}; - } - // Paper end - return new TickTask(this.tickCount, runnable); - } - - protected boolean shouldRun(TickTask ticktask) { - return ticktask.getTick() + 3 < this.tickCount || this.haveTime(); - } - - @Override - public boolean pollTask() { - boolean flag = this.pollTaskInternal(); - - this.mayHaveDelayedTasks = flag; - return flag; - } - - private boolean pollTaskInternal() { - if (super.pollTask()) { - this.executeMidTickTasks(); // Paper - execute chunk tasks mid tick - return true; - } else { - boolean ret = false; // Paper - force execution of all worlds, do not just bias the first - if (this.haveTime()) { - Iterator iterator = this.getAllLevels().iterator(); - - while (iterator.hasNext()) { - ServerLevel worldserver = (ServerLevel) iterator.next(); - - if (worldserver.getChunkSource().pollTask()) { - ret = true; // Paper - force execution of all worlds, do not just bias the first - } - } - } - - return ret; // Paper - force execution of all worlds, do not just bias the first - } - } - - public void doRunTask(TickTask ticktask) { // CraftBukkit - decompile error - this.getProfiler().incrementCounter("runTask"); - super.doRunTask(ticktask); - } - private void updateStatusIcon(ServerStatus metadata) { Optional optional = Optional.of(this.getFile("server-icon.png")).filter(File::isFile); @@ -1399,14 +1453,19 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { return !this.canOversleep(); - }); + // Gale start - base thread pool + }, 1_000_000L * this.nextTickTime - 200_000L); + isInSpareTime = false; + // Gale end - base thread pool isOversleep = false;MinecraftTimings.serverOversleep.stopTiming(); // Paper end new com.destroystokyo.paper.event.server.ServerTickStartEvent(this.tickCount+1).callEvent(); // Paper ++this.tickCount; + ScheduledMainThreadTaskQueues.shiftTasksForNextTick(); // Gale - base thread pool this.tickChildren(shouldKeepTicking); if (i - this.lastServerStatus >= 5000000000L) { this.lastServerStatus = i; @@ -1442,7 +1501,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0) { this.playerList.saveAll(playerSaveInterval); } - for (ServerLevel level : this.getAllLevels()) { + for (ServerLevel level : this.getAllLevelsArray()) { // Gale - base thread pool - optimize server levels if (level.paperConfig().chunks.autoSaveInterval.value() > 0) { level.saveIncrementally(fullSave); } @@ -1455,7 +1514,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0; // Paper worldserver.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper net.minecraft.world.level.block.entity.HopperBlockEntity.skipHopperEvents = worldserver.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper @@ -1609,7 +1666,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop, ServerLevel> newLevels = Maps.newLinkedHashMap(oldLevels); newLevels.put(level.dimension(), level); this.levels = Collections.unmodifiableMap(newLevels); + // Gale start - base thread pool - optimize server levels + this.levelArray = newLevels.values().toArray(this.levelArray); + for (int i = 0; i < this.levelArray.length; i++) { + this.levelArray[i].serverLevelArrayIndex = i; + } + this.overworld = null; + // Gale end - base thread pool - optimize server levels } public void removeLevel(ServerLevel level) { @@ -1638,6 +1707,14 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop, ServerLevel> newLevels = Maps.newLinkedHashMap(oldLevels); newLevels.remove(level.dimension()); this.levels = Collections.unmodifiableMap(newLevels); + // Gale start - base thread pool - optimize server levels + level.serverLevelArrayIndex = -1; + this.levelArray = newLevels.values().toArray(this.levelArray); + for (int i = 0; i < this.levelArray.length; i++) { + this.levelArray[i].serverLevelArrayIndex = i; + } + this.overworld = null; + // Gale end - base thread pool - optimize server levels } // CraftBukkit end @@ -1645,8 +1722,14 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop getAllLevels() { - return this.levels.values(); + return this.levels == null ? Collections.emptyList() : this.levels.values(); // Gale - base thread pool } public String getServerVersion() { @@ -1775,10 +1858,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop registryreadops, Thread thread, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PackRepository resourcepackrepository, WorldStem worldstem, DedicatedServerSettings dedicatedserversettings, DataFixer datafixer, Services services, ChunkProgressListenerFactory worldloadlistenerfactory) { + public DedicatedServer(joptsimple.OptionSet options, DataPackConfig datapackconfiguration, DynamicOps registryreadops, BaseThread thread, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PackRepository resourcepackrepository, WorldStem worldstem, DedicatedServerSettings dedicatedserversettings, DataFixer datafixer, Services services, ChunkProgressListenerFactory worldloadlistenerfactory) { // Gale - base thread pool super(options, datapackconfiguration, registryreadops, thread, convertable_conversionsession, resourcepackrepository, worldstem, Proxy.NO_PROXY, datafixer, services, worldloadlistenerfactory); // CraftBukkit end this.settings = dedicatedserversettings; diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java index 4654995f9982e77abe4b825b32312c2913671cd4..f833c02b92f995d3f9a09aa6a6594f8ceb800c6e 100644 --- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java +++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java @@ -11,7 +11,6 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Objects; -import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.function.BooleanSupplier; @@ -22,6 +21,7 @@ import net.minecraft.Util; import net.minecraft.core.BlockPos; import net.minecraft.core.SectionPos; import net.minecraft.network.protocol.Packet; +import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.progress.ChunkProgressListener; import net.minecraft.util.VisibleForDebug; import net.minecraft.util.profiling.ProfilerFiller; @@ -48,6 +48,7 @@ import net.minecraft.world.level.storage.DimensionDataStorage; import net.minecraft.world.level.storage.LevelData; import net.minecraft.world.level.storage.LevelStorageSource; import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; // Paper +import org.galemc.gale.executor.queue.AllLevelsScheduledChunkCacheTaskQueue; public class ServerChunkCache extends ChunkSource { @@ -994,6 +995,14 @@ public class ServerChunkCache extends ChunkSource { super.doRunTask(task); } + // Gale start - base thread pool + @Override + public void tell(Runnable runnable) { + super.tell(runnable); + MinecraftServer.nextTimeAssumeWeMayHaveDelayedTasks = true; + AllLevelsScheduledChunkCacheTaskQueue.signalReason.signalAnother(); + } + @Override // CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task public boolean pollTask() { diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java index 023cd8d948ff7360ac8348b980473ef119b41225..4869e23d7070649e216dcdc64687da61b670355a 100644 --- a/src/main/java/net/minecraft/server/level/ServerLevel.java +++ b/src/main/java/net/minecraft/server/level/ServerLevel.java @@ -22,6 +22,7 @@ import java.io.Writer; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; @@ -155,6 +156,9 @@ import net.minecraft.world.phys.shapes.BooleanOp; import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; import net.minecraft.world.ticks.LevelTicks; +import org.galemc.gale.executor.annotation.Access; +import org.galemc.gale.executor.annotation.AnyThreadSafe; +import org.galemc.gale.executor.annotation.MainThreadOnly; import org.slf4j.Logger; import org.bukkit.Bukkit; import org.bukkit.Location; @@ -188,6 +192,10 @@ public class ServerLevel extends Level implements WorldGenLevel { private static final int MAX_SCHEDULED_TICKS_PER_TICK = 65536; final List players; public final ServerChunkCache chunkSource; + // Gale start - base thread pool + @AnyThreadSafe(Access.READ) @MainThreadOnly(Access.WRITE) + public volatile int serverLevelArrayIndex; + // Gale end - base thread pool private final MinecraftServer server; public final PrimaryLevelData serverLevelData; // CraftBukkit - type final EntityTickList entityTickList; @@ -2609,7 +2617,7 @@ public class ServerLevel extends Level implements WorldGenLevel { // Spigot start if ( entity instanceof Player ) { - com.google.common.collect.Streams.stream( ServerLevel.this.getServer().getAllLevels() ).map( ServerLevel::getDataStorage ).forEach( (worldData) -> + Arrays.stream( ServerLevel.this.getServer().getAllLevelsArray() ).map( ServerLevel::getDataStorage ).forEach( (worldData) -> // Gale - base thread pool - optimize server levels { for (Object o : worldData.cache.values() ) { diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java index c171c272d5fcf0900514e18eafaa1b5ee019c74d..b9b9b14f5235d0e07feaa1dfedf254fa43880d6e 100644 --- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java @@ -187,6 +187,8 @@ import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; import org.apache.commons.lang3.StringUtils; import org.galemc.gale.configuration.GaleGlobalConfiguration; +import org.galemc.gale.executor.queue.BaseTaskQueues; +import org.galemc.gale.executor.queue.ScheduledMainThreadTaskQueues; import org.slf4j.Logger; // CraftBukkit start @@ -556,7 +558,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic Objects.requireNonNull(this.connection); // CraftBukkit - Don't wait - minecraftserver.scheduleOnMain(networkmanager::handleDisconnection); // Paper + ScheduledMainThreadTaskQueues.add(networkmanager::handleDisconnection, ScheduledMainThreadTaskQueues.HANDLE_DISCONNECT_TASK_MAX_DELAY); // Paper // Gale - base thread pool } private CompletableFuture filterTextPacket(T text, BiFunction> filterer) { @@ -887,21 +889,20 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic } // Paper start - private static final java.util.concurrent.ExecutorService TAB_COMPLETE_EXECUTOR = java.util.concurrent.Executors.newFixedThreadPool(4, - new com.google.common.util.concurrent.ThreadFactoryBuilder().setDaemon(true).setNameFormat("Async Tab Complete Thread - #%d").setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(net.minecraft.server.MinecraftServer.LOGGER)).build()); + private static final java.util.concurrent.ExecutorService TAB_COMPLETE_EXECUTOR = BaseTaskQueues.scheduledAsync.yieldingExecutor; // Gale - base thread pool - remove tab complete executor // Paper end @Override public void handleCustomCommandSuggestions(ServerboundCommandSuggestionPacket packet) { // PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel()); // Paper - run this async // CraftBukkit start if (this.chatSpamTickCount.addAndGet(io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.tabSpamIncrement) > io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.tabSpamLimit && !this.server.getPlayerList().isOp(this.player.getGameProfile())) { // Paper start - split and make configurable - server.scheduleOnMain(() -> this.disconnect(Component.translatable("disconnect.spam", ArrayConstants.emptyObjectArray), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM)); // Paper - kick event cause // Gale - JettPack - reduce array allocations + ScheduledMainThreadTaskQueues.add(() -> this.disconnect(Component.translatable("disconnect.spam", ArrayConstants.emptyObjectArray), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM), ScheduledMainThreadTaskQueues.KICK_FOR_COMMAND_PACKET_SPAM_TASK_MAX_DELAY); // Paper - kick event cause // Gale - JettPack - reduce array allocations // Gale - base thread pool return; } // Paper start String str = packet.getCommand(); int index = -1; if (str.length() > 64 && ((index = str.indexOf(' ')) == -1 || index >= 64)) { - server.scheduleOnMain(() -> this.disconnect(Component.translatable("disconnect.spam", ArrayConstants.emptyObjectArray), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM)); // Paper - kick event cause // Gale - JettPack - reduce array allocations + ScheduledMainThreadTaskQueues.add(() -> this.disconnect(Component.translatable("disconnect.spam", ArrayConstants.emptyObjectArray), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM), ScheduledMainThreadTaskQueues.KICK_FOR_COMMAND_PACKET_SPAM_TASK_MAX_DELAY); // Paper - kick event cause // Gale - JettPack - reduce array allocations // Gale - base thread pool return; } // Paper end @@ -926,7 +927,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic if (!event.isHandled()) { if (!event.isCancelled()) { - this.server.scheduleOnMain(() -> { // This needs to be on main + ScheduledMainThreadTaskQueues.add(() -> { // This needs to be on main // Gale - base thread pool ParseResults parseresults = this.server.getCommands().getDispatcher().parse(stringreader, this.player.createCommandSourceStack()); this.server.getCommands().getDispatcher().getCompletionSuggestions(parseresults).thenAccept((suggestions) -> { @@ -937,7 +938,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic this.connection.send(new ClientboundCommandSuggestionsPacket(packet.getId(), suggestEvent.getSuggestions())); // Paper end - Brigadier API }); - }); + }, ScheduledMainThreadTaskQueues.SEND_COMMAND_COMPLETION_SUGGESTIONS_TASK_MAX_DELAY); // Gale - base thread pool } } else if (!completions.isEmpty()) { final com.mojang.brigadier.suggestion.SuggestionsBuilder builder0 = new com.mojang.brigadier.suggestion.SuggestionsBuilder(command, stringreader.getTotalLength()); @@ -1246,7 +1247,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic int byteLength = testString.getBytes(java.nio.charset.StandardCharsets.UTF_8).length; if (byteLength > 256 * 4) { ServerGamePacketListenerImpl.LOGGER.warn(this.player.getScoreboardName() + " tried to send a book with with a page too large!"); - server.scheduleOnMain(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause + ScheduledMainThreadTaskQueues.add(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION), ScheduledMainThreadTaskQueues.KICK_FOR_BOOK_TOO_LARGE_PACKET_TASK_MAX_DELAY); // Paper - kick event cause // Gale - base thread pool return; } byteTotal += byteLength; @@ -1269,14 +1270,14 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic if (byteTotal > byteAllowed) { ServerGamePacketListenerImpl.LOGGER.warn(this.player.getScoreboardName() + " tried to send too large of a book. Book Size: " + byteTotal + " - Allowed: "+ byteAllowed + " - Pages: " + pageList.size()); - server.scheduleOnMain(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause + ScheduledMainThreadTaskQueues.add(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION), ScheduledMainThreadTaskQueues.KICK_FOR_BOOK_TOO_LARGE_PACKET_TASK_MAX_DELAY); // Paper - kick event cause // Gale - base thread pool return; } } // Paper end // CraftBukkit start if (this.lastBookTick + 20 > MinecraftServer.currentTick) { - server.scheduleOnMain(() -> this.disconnect("Book edited too quickly!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause // Paper - Also ensure this is called on main + ScheduledMainThreadTaskQueues.add(() -> this.disconnect("Book edited too quickly!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION), ScheduledMainThreadTaskQueues.KICK_FOR_EDITING_BOOK_TOO_QUICKLY_TASK_MAX_DELAY); // Paper - kick event cause // Paper - Also ensure this is called on main // Gale - base thread pool return; } this.lastBookTick = MinecraftServer.currentTick; @@ -2077,10 +2078,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic public void handleTeleportToEntityPacket(ServerboundTeleportToEntityPacket packet) { PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel()); if (this.player.isSpectator()) { - Iterator iterator = this.server.getAllLevels().iterator(); - - while (iterator.hasNext()) { - ServerLevel worldserver = (ServerLevel) iterator.next(); + for (ServerLevel worldserver : this.server.getAllLevelsArray()) { // Gale - base thread pool - optimize server levels Entity entity = packet.getEntity(worldserver); if (entity != null) { @@ -2228,9 +2226,9 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic } // CraftBukkit end if (ServerGamePacketListenerImpl.isChatMessageIllegal(packet.message())) { - this.server.scheduleOnMain(() -> { // Paper - push to main for event firing + ScheduledMainThreadTaskQueues.add(() -> { // Paper - push to main for event firing // Gale - base thread pool this.disconnect(Component.translatable("multiplayer.disconnect.illegal_characters"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_CHARACTERS); // Paper - add cause - }); // Paper - push to main for event firing + }, ScheduledMainThreadTaskQueues.KICK_FOR_ILLEGAL_CHARACTERS_IN_CHAT_PACKET_TASK_MAX_DELAY); // Paper - push to main for event firing // Gale - base thread pool } else { if (this.tryHandleChat(packet.message(), packet.timeStamp(), packet.lastSeenMessages())) { // this.server.submit(() -> { // CraftBukkit - async chat @@ -2258,9 +2256,9 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic @Override public void handleChatCommand(ServerboundChatCommandPacket packet) { if (ServerGamePacketListenerImpl.isChatMessageIllegal(packet.command())) { - this.server.scheduleOnMain(() -> { // Paper - push to main for event firing + ScheduledMainThreadTaskQueues.add(() -> { // Paper - push to main for event firing // Gale - base thread pool this.disconnect(Component.translatable("multiplayer.disconnect.illegal_characters"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_CHARACTERS); // Paper - }); // Paper - push to main for event firing + }, ScheduledMainThreadTaskQueues.KICK_FOR_ILLEGAL_CHARACTERS_IN_CHAT_PACKET_TASK_MAX_DELAY); // Paper - push to main for event firing // Gale - base thread pool } else { if (this.tryHandleChat(packet.command(), packet.timeStamp(), packet.lastSeenMessages())) { this.server.submit(() -> { @@ -2357,9 +2355,9 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic private boolean tryHandleChat(String message, Instant timestamp, LastSeenMessages.Update acknowledgment) { if (!this.updateChatOrder(timestamp)) { ServerGamePacketListenerImpl.LOGGER.warn("{} sent out-of-order chat: '{}': {} > {}", this.player.getName().getString(), message, this.lastChatTimeStamp.get().getEpochSecond(), timestamp.getEpochSecond()); // Paper - this.server.scheduleOnMain(() -> { // Paper - push to main + ScheduledMainThreadTaskQueues.add(() -> { // Paper - push to main // Gale - base thread pool this.disconnect(Component.translatable("multiplayer.disconnect.out_of_order_chat"), org.bukkit.event.player.PlayerKickEvent.Cause.OUT_OF_ORDER_CHAT); // Paper - kick event cause - }); // Paper - push to main + }, ScheduledMainThreadTaskQueues.KICK_FOR_OUT_OF_ORDER_CHAT_PACKET_TASK_MAX_DELAY); // Paper - push to main // Gale - base thread pool return false; } else if (this.player.isRemoved() || this.player.getChatVisibility() == ChatVisiblity.HIDDEN) { // CraftBukkit - dead men tell no tales this.send(new ClientboundSystemChatPacket(Component.translatable("chat.disabled.options").withStyle(ChatFormatting.RED), false)); @@ -3420,7 +3418,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic // Paper start if (!org.bukkit.Bukkit.isPrimaryThread()) { if (recipeSpamPackets.addAndGet(io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.recipeSpamIncrement) > io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.recipeSpamLimit) { - server.scheduleOnMain(() -> this.disconnect(net.minecraft.network.chat.Component.translatable("disconnect.spam", ArrayConstants.emptyObjectArray), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM)); // Paper - kick event cause // Gale - JettPack - reduce array allocations + ScheduledMainThreadTaskQueues.add(() -> this.disconnect(net.minecraft.network.chat.Component.translatable("disconnect.spam", ArrayConstants.emptyObjectArray), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM), ScheduledMainThreadTaskQueues.KICK_FOR_RECIPE_PACKET_SPAM_TASK_MAX_DELAY); // Paper - kick event cause // Gale - JettPack - reduce array allocations // Gale - base thread pool return; } } diff --git a/src/main/java/net/minecraft/server/network/TextFilterClient.java b/src/main/java/net/minecraft/server/network/TextFilterClient.java index 2393b6a5f3f12c2b17b172ee8ca42ead218e2a10..ff2d2366966b00436a350ec73819930248a0fc4f 100644 --- a/src/main/java/net/minecraft/server/network/TextFilterClient.java +++ b/src/main/java/net/minecraft/server/network/TextFilterClient.java @@ -23,7 +23,6 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; import javax.annotation.Nullable; @@ -32,6 +31,7 @@ import net.minecraft.Util; import net.minecraft.network.chat.FilterMask; import net.minecraft.util.GsonHelper; import net.minecraft.util.thread.ProcessorMailbox; +import org.galemc.gale.executor.queue.BaseTaskQueues; import org.slf4j.Logger; public class TextFilterClient implements AutoCloseable { @@ -62,7 +62,7 @@ public class TextFilterClient implements AutoCloseable { this.joinEncoder = joinEncoder; this.leaveEndpoint = leaveEndpoint; this.leaveEncoder = leaveEncoder; - this.workerPool = Executors.newFixedThreadPool(parallelism, THREAD_FACTORY); + this.workerPool = BaseTaskQueues.scheduledAsync.yieldingExecutor; // Gale - base thread pool - remove text filter executor } private static URL getEndpoint(URI root, @Nullable JsonObject endpoints, String key, String fallback) throws MalformedURLException { diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java index a60db92d8c6afab40e12b3ac28241beac06bcf63..e28f69ca50b513faa7eb656992be14ab8ee50291 100644 --- a/src/main/java/net/minecraft/server/players/PlayerList.java +++ b/src/main/java/net/minecraft/server/players/PlayerList.java @@ -15,7 +15,6 @@ import java.net.SocketAddress; import java.nio.file.Path; import java.text.SimpleDateFormat; import java.time.Instant; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; @@ -101,10 +100,10 @@ import net.minecraft.world.scores.PlayerTeam; import net.minecraft.world.scores.Scoreboard; // Paper import net.minecraft.world.scores.Team; import org.galemc.gale.configuration.GaleGlobalConfiguration; +import org.galemc.gale.executor.queue.ScheduledMainThreadTaskQueues; import org.slf4j.Logger; // CraftBukkit start -import java.util.stream.Collectors; import net.minecraft.server.dedicated.DedicatedServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; @@ -306,7 +305,7 @@ public abstract class PlayerList { worldserver1, chunkX, chunkZ, net.minecraft.server.level.ChunkHolder.FullChunkStatus.ENTITY_TICKING, true, ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHEST, (chunk) -> { - MinecraftServer.getServer().scheduleOnMain(() -> { + ScheduledMainThreadTaskQueues.add(() -> { // Gale - base thread pool try { if (!playerconnection.connection.isConnected()) { return; @@ -319,7 +318,7 @@ public abstract class PlayerList { } finally { finalWorldserver.pendingLogin.remove(player); } - }); + }, ScheduledMainThreadTaskQueues.POST_CHUNK_LOAD_JOIN_TASK_MAX_DELAY); // Gale - base thread pool } ); } @@ -1578,10 +1577,8 @@ public abstract class PlayerList { public void setViewDistance(int viewDistance) { this.viewDistance = viewDistance; //this.broadcastAll(new ClientboundSetChunkCacheRadiusPacket(viewDistance)); // Paper - move into setViewDistance - Iterator iterator = this.server.getAllLevels().iterator(); - while (iterator.hasNext()) { - ServerLevel worldserver = (ServerLevel) iterator.next(); + for (ServerLevel worldserver : this.server.getAllLevelsArray()) { // Gale - base thread pool - optimize server levels if (worldserver != null) { worldserver.getChunkSource().setViewDistance(viewDistance); @@ -1593,10 +1590,8 @@ public abstract class PlayerList { public void setSimulationDistance(int simulationDistance) { this.simulationDistance = simulationDistance; //this.broadcastAll(new ClientboundSetSimulationDistancePacket(simulationDistance)); // Paper - handled by playerchunkloader - Iterator iterator = this.server.getAllLevels().iterator(); - while (iterator.hasNext()) { - ServerLevel worldserver = (ServerLevel) iterator.next(); + for (ServerLevel worldserver : this.server.getAllLevelsArray()) { // Gale - base thread pool - optimize server levels if (worldserver != null) { worldserver.getChunkSource().setSimulationDistance(simulationDistance); diff --git a/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java b/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java index 83701fbfaa56a232593ee8f11a3afb8941238bfa..392e7b4a89669f16b32043b65b69e6593d17f10e 100644 --- a/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java +++ b/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java @@ -6,17 +6,18 @@ import com.mojang.logging.LogUtils; import java.util.List; import java.util.Queue; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; import java.util.concurrent.locks.LockSupport; import java.util.function.BooleanSupplier; import java.util.function.Supplier; + import net.minecraft.util.profiling.metrics.MetricCategory; import net.minecraft.util.profiling.metrics.MetricSampler; import net.minecraft.util.profiling.metrics.MetricsRegistry; import net.minecraft.util.profiling.metrics.ProfilerMeasured; +import org.galemc.gale.executor.AbstractBlockableEventLoop; import org.slf4j.Logger; -public abstract class BlockableEventLoop implements ProfilerMeasured, ProcessorHandle, Executor { +public abstract class BlockableEventLoop implements ProfilerMeasured, ProcessorHandle, AbstractBlockableEventLoop { // Gale - base thread pool private final String name; private static final Logger LOGGER = LogUtils.getLogger(); private final Queue pendingRunnables = Queues.newConcurrentLinkedQueue(); @@ -31,6 +32,7 @@ public abstract class BlockableEventLoop implements Profiler protected abstract boolean shouldRun(R task); + @Override // Gale - base thread pool public boolean isSameThread() { return Thread.currentThread() == this.getRunningThread(); } @@ -45,6 +47,12 @@ public abstract class BlockableEventLoop implements Profiler return this.pendingRunnables.size(); } + // Gale start - base thread pool + public boolean hasPendingTasks() { + return !this.pendingRunnables.isEmpty(); + } + // Gale end - base thread pool + @Override public String name() { return this.name; @@ -102,6 +110,7 @@ public abstract class BlockableEventLoop implements Profiler } + @Override // Gale - base thread pool public void executeIfPossible(Runnable runnable) { this.execute(runnable); } diff --git a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java index ea23e771ab2b77e8001d0eaaf834423353ef70c2..4fc0deca1ba4da302f40cbf8847420d0042b44da 100644 --- a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java +++ b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java @@ -97,7 +97,7 @@ public abstract class Projectile extends Entity { this.cachedOwner = ((ServerLevel) this.level).getEntity(this.ownerUUID); // Paper start - check all worlds if (this.cachedOwner == null) { - for (final ServerLevel level : this.level.getServer().getAllLevels()) { + for (final ServerLevel level : this.level.getServer().getAllLevelsArray()) { // Gale - base thread pool - optimize server levels if (level == this.level) continue; final Entity entity = level.getEntity(this.ownerUUID); if (entity != null) { diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java index 69bde99acff7bdae9af7cfe60e2221675a68b858..eb61050067c0f182ef50f4c382add3ea03019701 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java @@ -969,7 +969,7 @@ public final class CraftServer implements Server { org.spigotmc.SpigotConfig.init((File) console.options.valueOf("spigot-settings")); // Spigot this.console.paperConfigurations.reloadConfigs(this.console); this.console.galeConfigurations.reloadConfigs(this.console); // Gale - Gale configuration - for (ServerLevel world : this.console.getAllLevels()) { + for (ServerLevel world : this.console.getAllLevelsArray()) { // Gale - base thread pool - optimize server levels // world.serverLevelData.setDifficulty(config.difficulty); // Paper - per level difficulty world.setSpawnSettings(world.serverLevelData.getDifficulty() != Difficulty.PEACEFUL && config.spawnMonsters, config.spawnAnimals); // Paper - per level difficulty (from MinecraftServer#setDifficulty(ServerLevel, Difficulty, boolean)) @@ -1153,7 +1153,7 @@ public final class CraftServer implements Server { @Override public World createWorld(WorldCreator creator) { - Preconditions.checkState(this.console.getAllLevels().iterator().hasNext(), "Cannot create additional worlds on STARTUP"); + Preconditions.checkState(this.console.getAllLevelsArray().length > 0, "Cannot create additional worlds on STARTUP"); // Gale - base thread pool - optimize server levels //Preconditions.checkState(!this.console.isIteratingOverLevels, "Cannot create a world while worlds are being ticked"); // Paper - Cat - Temp disable. We'll see how this goes. Validate.notNull(creator, "Creator may not be null"); @@ -2498,7 +2498,7 @@ public final class CraftServer implements Server { public Entity getEntity(UUID uuid) { Validate.notNull(uuid, "UUID cannot be null"); - for (ServerLevel world : this.getServer().getAllLevels()) { + for (ServerLevel world : this.getServer().getAllLevelsArray()) { // Gale - base thread pool - optimize server levels net.minecraft.world.entity.Entity entity = world.getEntity(uuid); if (entity != null) { return entity.getBukkitEntity(); diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java index 55d83a9a691d11c9408d2c3260c3e77dfb51b97e..89edfcf25e0359905e26181262b9ea5d7b99c56c 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java @@ -5,7 +5,6 @@ import com.google.common.base.Predicates; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.mojang.datafixers.util.Pair; -import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import java.io.File; @@ -21,7 +20,6 @@ import java.util.Objects; import java.util.Random; import java.util.Set; import java.util.UUID; -import java.util.concurrent.ExecutionException; import java.util.function.Predicate; import java.util.stream.Collectors; import net.minecraft.core.BlockPos; @@ -43,7 +41,6 @@ import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.Ticket; import net.minecraft.server.level.TicketType; import net.minecraft.sounds.SoundSource; -import net.minecraft.tags.TagKey; import net.minecraft.util.SortedArraySet; import net.minecraft.util.Unit; import net.minecraft.world.entity.EntityType; @@ -115,7 +112,6 @@ import org.bukkit.entity.TippedArrow; import org.bukkit.entity.Trident; import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason; import org.bukkit.event.weather.LightningStrikeEvent; -import org.bukkit.event.world.SpawnChangeEvent; import org.bukkit.event.world.TimeSkipEvent; import org.bukkit.generator.BiomeProvider; import org.bukkit.generator.BlockPopulator; @@ -135,6 +131,7 @@ import org.bukkit.util.Consumer; import org.bukkit.util.RayTraceResult; import org.bukkit.util.StructureSearchResult; import org.bukkit.util.Vector; +import org.galemc.gale.executor.queue.ScheduledMainThreadTaskQueues; public class CraftWorld extends CraftRegionAccessor implements World { public static final int CUSTOM_DIMENSION_OFFSET = 10; @@ -2353,11 +2350,11 @@ public class CraftWorld extends CraftRegionAccessor implements World { java.util.concurrent.CompletableFuture ret = new java.util.concurrent.CompletableFuture<>(); io.papermc.paper.chunk.system.ChunkSystem.scheduleChunkLoad(this.getHandle(), x, z, gen, ChunkStatus.FULL, true, priority, (c) -> { - net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> { + ScheduledMainThreadTaskQueues.add(() -> { // Gale - base thread pool net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk)c; if (chunk != null) addTicket(x, z); // Paper ret.complete(chunk == null ? null : chunk.getBukkitChunk()); - }); + }, ScheduledMainThreadTaskQueues.COMPLETE_CHUNK_FUTURE_TASK_MAX_DELAY); // Gale - base thread pool }); return ret; diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java index 9368ec01e498f913bc5b7b3e77fe87659090d9b5..54b2a93a2b4708a2e06fa399064105f8f95402ca 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java @@ -189,6 +189,7 @@ import org.bukkit.plugin.Plugin; import org.bukkit.util.BoundingBox; import org.bukkit.util.NumberConversions; import org.bukkit.util.Vector; +import org.galemc.gale.executor.queue.ScheduledMainThreadTaskQueues; public abstract class CraftEntity implements org.bukkit.entity.Entity { private static PermissibleBase perm; @@ -1260,7 +1261,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { for (net.minecraft.world.level.chunk.ChunkAccess chunk : list) { chunkProviderServer.addTicketAtLevel(net.minecraft.server.level.TicketType.POST_TELEPORT, chunk.getPos(), 33, CraftEntity.this.getEntityId()); } - net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> { + ScheduledMainThreadTaskQueues.add(() -> { // Gale - base thread pool try { ret.complete(CraftEntity.this.teleport(locationClone, cause) ? Boolean.TRUE : Boolean.FALSE); } catch (Throwable throwable) { @@ -1270,7 +1271,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { net.minecraft.server.MinecraftServer.LOGGER.error("Failed to teleport entity " + CraftEntity.this, throwable); ret.completeExceptionally(throwable); } - }); + }, ScheduledMainThreadTaskQueues.TELEPORT_ASYNC_TASK_MAX_DELAY); // Gale - base thread pool }); return ret; diff --git a/src/main/java/org/galemc/gale/concurrent/LockAndCondition.java b/src/main/java/org/galemc/gale/concurrent/LockAndCondition.java index 73fd8ca0bd1168862a03d9bdcae93d62895e8c1f..6985e86495fc7046cbe1623d90f72adc95d909e2 100644 --- a/src/main/java/org/galemc/gale/concurrent/LockAndCondition.java +++ b/src/main/java/org/galemc/gale/concurrent/LockAndCondition.java @@ -9,7 +9,7 @@ import java.util.concurrent.locks.Lock; * A utility class that stores a {@link Condition} with its {@link Lock}, that can be passed around and used instead * of using an {@link Object} monitor, which does not support speculative locking. * - * @author Martijn Muijsers + * @author Martijn Muijsers under AGPL-3.0 */ public class LockAndCondition { diff --git a/src/main/java/org/galemc/gale/concurrent/Mutex.java b/src/main/java/org/galemc/gale/concurrent/Mutex.java index 76e4d7b2f242218df7e853b416a69d62707357e8..beb233dee5b00eee1ff3b3bc9818a2a70a4421d3 100644 --- a/src/main/java/org/galemc/gale/concurrent/Mutex.java +++ b/src/main/java/org/galemc/gale/concurrent/Mutex.java @@ -17,7 +17,7 @@ import java.util.concurrent.locks.Lock; * respectively {@link #acquireUninterruptibly}, {@link #acquire}, {@link #tryAcquire} and * {@link #release}. The {@link Lock#newCondition} method does not have a default implementation. * - * @author Martijn Muijsers + * @author Martijn Muijsers under AGPL-3.0 */ @AnyThreadSafe public interface Mutex extends Lock { diff --git a/src/main/java/org/galemc/gale/concurrent/SemaphoreMutex.java b/src/main/java/org/galemc/gale/concurrent/SemaphoreMutex.java index f8e3151e6ba1ef0850f50c836962b33f088de375..03bf3a93f683010ea76bbb26ae03c18e4e98d889 100644 --- a/src/main/java/org/galemc/gale/concurrent/SemaphoreMutex.java +++ b/src/main/java/org/galemc/gale/concurrent/SemaphoreMutex.java @@ -15,7 +15,7 @@ import java.util.concurrent.locks.Lock; * and throws {@link UnsupportedOperationException} for all {@link Lock} methods that do not have a default * implementation in {@link Mutex}. * - * @author Martijn Muijsers + * @author Martijn Muijsers under AGPL-3.0 */ @AnyThreadSafe @YieldFree public class SemaphoreMutex extends Semaphore implements Mutex { diff --git a/src/main/java/org/galemc/gale/configuration/GaleConfigurations.java b/src/main/java/org/galemc/gale/configuration/GaleConfigurations.java index c7a4186bb93992733f393ac58fb4d65bbc8db861..c00186a73dc5c705a8a76e6ee532cd63cdf11b4d 100644 --- a/src/main/java/org/galemc/gale/configuration/GaleConfigurations.java +++ b/src/main/java/org/galemc/gale/configuration/GaleConfigurations.java @@ -266,7 +266,7 @@ public class GaleConfigurations extends Configurations + *
  • Default: -1
  • + *
  • Vanilla: -1
  • + * + */ + @Setting("default") + public int defaultValue = -1; + + /** + * The default maximum delay for completing a {@link java.util.concurrent.CompletableFuture} + * for a chunk load, after the chunk has already finished loading. + * Given in ticks. + * Any value < 0 uses {@link #defaultValue}. + *
      + *
    • Default: 0
    • + *
    • Vanilla: -1
    • + *
    + */ + public int completeChunkFuture = 0; + + /** + * The default maximum delay for completing the steps needed to take when a player is joining and the + * necessary chunk has been loaded. + * Given in ticks. + * Any value < 0 uses {@link #defaultValue}. + *
      + *
    • Default: 19
    • + *
    • Vanilla: -1
    • + *
    + */ + public int postChunkLoadJoin = 19; + + /** + * The default maximum delay for chunk packets to be modified for anti-xray. + * Given in ticks. + * Any value < 0 uses {@link #defaultValue}. + *
      + *
    • Default: 19
    • + *
    • Vanilla: -1
    • + *
    + */ + public int antiXrayModifyBlocks = 19; + + /** + * The default maximum delay for entities to be teleported when a teleport is started asynchronously. + * Given in ticks. + * Any value < 0 uses {@link #defaultValue}. + *
      + *
    • Default: -1
    • + *
    • Vanilla: -1
    • + *
    + */ + public int teleportAsync = -1; + + /** + * The default maximum delay for command completion suggestions to be sent to the player. + * Any value < 0 uses {@link #defaultValue}. + *
      + *
    • Default: 9
    • + *
    • Vanilla: -1
    • + *
    + */ + public int sendCommandCompletionSuggestions = 9; + + /** + * The default maximum delay for players to get kicked for command packet spam. + * Given in ticks. + * Any value < 0 uses {@link #defaultValue}. + *
      + *
    • Default: 0
    • + *
    • Vanilla: -1
    • + *
    + */ + public int kickForCommandPacketSpam = 0; + + /** + * The default maximum delay for players to get kicked for place-recipe packet spam. + * Given in ticks. + * Any value < 0 uses {@link #defaultValue}. + *
      + *
    • Default: 0
    • + *
    • Vanilla: -1
    • + *
    + */ + public int kickForRecipePacketSpam = 0; + + /** + * The default maximum delay for players to get kicked for sending invalid packets trying to + * send book content that is too large, which usually indicates they are attempting to abuse an exploit. + * Given in ticks. + * Any value < 0 uses {@link #defaultValue}. + *
      + *
    • Default: -1
    • + *
    • Vanilla: -1
    • + *
    + */ + public int kickForBookTooLargePacket = -1; + + /** + * The default maximum delay for players to get kicked for editing a book too quickly. + * Given in ticks. + * Any value < 0 uses {@link #defaultValue}. + *
      + *
    • Default: -1
    • + *
    • Vanilla: -1
    • + *
    + */ + public int kickForEditingBookTooQuickly = -1; + + /** + * The default maximum delay for players to get kicked for sending a chat packet with illegal characters. + * Given in ticks. + * Any value < 0 uses {@link #defaultValue}. + *
      + *
    • Default: -1
    • + *
    • Vanilla: -1
    • + *
    + */ + public int kickForIllegalCharactersInChatPacket = -1; + + /** + * The default maximum delay for players to get kicked for sending an out-of-order chat packet. + * Given in ticks. + * Any value < 0 uses {@link #defaultValue}. + *
      + *
    • Default: -1
    • + *
    • Vanilla: -1
    • + *
    + */ + public int kickForOutOfOrderChatPacket = -1; + + /** + * The default maximum delay for handling player disconnects. + * Any value < 0 uses {@link #defaultValue}. + *
      + *
    • Default: -1
    • + *
    • Vanilla: -1
    • + *
    + */ + public int handleDisconnect = -1; + + @Override + public void postProcess() { + while (!ScheduledMainThreadTaskQueues.writeLock.tryLock()); + try { + // Update the values in MinecraftServerBlockableEventLoop for quick access + ScheduledMainThreadTaskQueues.DEFAULT_TASK_MAX_DELAY = this.defaultValue >= 0 ? this.defaultValue : 2; + ScheduledMainThreadTaskQueues.COMPLETE_CHUNK_FUTURE_TASK_MAX_DELAY = this.completeChunkFuture >= 0 ? this.completeChunkFuture : ScheduledMainThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledMainThreadTaskQueues.POST_CHUNK_LOAD_JOIN_TASK_MAX_DELAY = this.postChunkLoadJoin >= 0 ? this.postChunkLoadJoin : ScheduledMainThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledMainThreadTaskQueues.ANTI_XRAY_MODIFY_BLOCKS_TASK_MAX_DELAY = this.antiXrayModifyBlocks >= 0 ? this.antiXrayModifyBlocks : ScheduledMainThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledMainThreadTaskQueues.TELEPORT_ASYNC_TASK_MAX_DELAY = this.teleportAsync >= 0 ? this.teleportAsync : ScheduledMainThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledMainThreadTaskQueues.SEND_COMMAND_COMPLETION_SUGGESTIONS_TASK_MAX_DELAY = this.sendCommandCompletionSuggestions >= 0 ? this.sendCommandCompletionSuggestions : ScheduledMainThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledMainThreadTaskQueues.KICK_FOR_COMMAND_PACKET_SPAM_TASK_MAX_DELAY = this.kickForCommandPacketSpam >= 0 ? this.kickForCommandPacketSpam : ScheduledMainThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledMainThreadTaskQueues.KICK_FOR_RECIPE_PACKET_SPAM_TASK_MAX_DELAY = this.kickForRecipePacketSpam >= 0 ? this.kickForRecipePacketSpam : ScheduledMainThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledMainThreadTaskQueues.KICK_FOR_BOOK_TOO_LARGE_PACKET_TASK_MAX_DELAY = this.kickForBookTooLargePacket >= 0 ? this.kickForBookTooLargePacket : ScheduledMainThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledMainThreadTaskQueues.KICK_FOR_EDITING_BOOK_TOO_QUICKLY_TASK_MAX_DELAY = this.kickForEditingBookTooQuickly >= 0 ? this.kickForEditingBookTooQuickly : ScheduledMainThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledMainThreadTaskQueues.KICK_FOR_ILLEGAL_CHARACTERS_IN_CHAT_PACKET_TASK_MAX_DELAY = this.kickForIllegalCharactersInChatPacket >= 0 ? this.kickForIllegalCharactersInChatPacket : ScheduledMainThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledMainThreadTaskQueues.KICK_FOR_OUT_OF_ORDER_CHAT_PACKET_TASK_MAX_DELAY = this.kickForOutOfOrderChatPacket >= 0 ? this.kickForOutOfOrderChatPacket : ScheduledMainThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledMainThreadTaskQueues.HANDLE_DISCONNECT_TASK_MAX_DELAY = this.handleDisconnect >= 0 ? this.handleDisconnect : ScheduledMainThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + // Change the length of the pendingRunnables array of queues + int maxDelay = 0; + for (int delay : new int[]{ + ScheduledMainThreadTaskQueues.DEFAULT_TASK_MAX_DELAY, + ScheduledMainThreadTaskQueues.COMPLETE_CHUNK_FUTURE_TASK_MAX_DELAY, + ScheduledMainThreadTaskQueues.POST_CHUNK_LOAD_JOIN_TASK_MAX_DELAY, + ScheduledMainThreadTaskQueues.ANTI_XRAY_MODIFY_BLOCKS_TASK_MAX_DELAY, + ScheduledMainThreadTaskQueues.TELEPORT_ASYNC_TASK_MAX_DELAY, + ScheduledMainThreadTaskQueues.SEND_COMMAND_COMPLETION_SUGGESTIONS_TASK_MAX_DELAY, + ScheduledMainThreadTaskQueues.KICK_FOR_COMMAND_PACKET_SPAM_TASK_MAX_DELAY, + ScheduledMainThreadTaskQueues.KICK_FOR_RECIPE_PACKET_SPAM_TASK_MAX_DELAY, + ScheduledMainThreadTaskQueues.KICK_FOR_BOOK_TOO_LARGE_PACKET_TASK_MAX_DELAY, + ScheduledMainThreadTaskQueues.KICK_FOR_EDITING_BOOK_TOO_QUICKLY_TASK_MAX_DELAY, + ScheduledMainThreadTaskQueues.KICK_FOR_ILLEGAL_CHARACTERS_IN_CHAT_PACKET_TASK_MAX_DELAY, + ScheduledMainThreadTaskQueues.KICK_FOR_OUT_OF_ORDER_CHAT_PACKET_TASK_MAX_DELAY, + ScheduledMainThreadTaskQueues.HANDLE_DISCONNECT_TASK_MAX_DELAY + }) { + if (delay > maxDelay) { + maxDelay = delay; + } + } + int newPendingRunnablesLength = maxDelay + 1; + int oldPendingRunnablesLength = ScheduledMainThreadTaskQueues.queues.length; + if (oldPendingRunnablesLength != newPendingRunnablesLength) { + if (oldPendingRunnablesLength > newPendingRunnablesLength) { + // Move all tasks in queues that will be removed to the last queue + for (int i = newPendingRunnablesLength + 1; i < ScheduledMainThreadTaskQueues.queues.length; i++) { + ScheduledMainThreadTaskQueues.queues[maxDelay].addAll(ScheduledMainThreadTaskQueues.queues[i]); + } + // Update the first queue with elements index + if (ScheduledMainThreadTaskQueues.firstQueueWithPotentialTasksIndex >= newPendingRunnablesLength) { + ScheduledMainThreadTaskQueues.firstQueueWithPotentialTasksIndex = maxDelay; + } + } + ScheduledMainThreadTaskQueues.queues = Arrays.copyOf(ScheduledMainThreadTaskQueues.queues, newPendingRunnablesLength); + if (newPendingRunnablesLength > oldPendingRunnablesLength) { + // Create new queues + for (int i = oldPendingRunnablesLength; i < newPendingRunnablesLength; i++) { + ScheduledMainThreadTaskQueues.queues[i] = new MultiThreadedQueue<>(); + } + } + } + } finally { + ScheduledMainThreadTaskQueues.writeLock.unlock(); + } + } + + } + // Gale end - base thread pool + } public LogToConsole logToConsole; diff --git a/src/main/java/org/galemc/gale/executor/AbstractBlockableEventLoop.java b/src/main/java/org/galemc/gale/executor/AbstractBlockableEventLoop.java new file mode 100644 index 0000000000000000000000000000000000000000..c8208f47c53df8ee438409b0a954c9dafca6b8fa --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/AbstractBlockableEventLoop.java @@ -0,0 +1,20 @@ +// Gale - base thread pool + +package org.galemc.gale.executor; + +import net.minecraft.util.thread.BlockableEventLoop; + +import java.util.concurrent.Executor; + +/** + * An interface for the common functionality of {@link BlockableEventLoop} and {@link MinecraftServerBlockableEventLoop}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public interface AbstractBlockableEventLoop extends Executor { + + boolean isSameThread(); + + void executeIfPossible(Runnable runnable); + +} diff --git a/src/main/java/org/galemc/gale/executor/MinecraftServerBlockableEventLoop.java b/src/main/java/org/galemc/gale/executor/MinecraftServerBlockableEventLoop.java new file mode 100644 index 0000000000000000000000000000000000000000..fe168eb59694415b84ee83f943a7e24b1f3d09da --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/MinecraftServerBlockableEventLoop.java @@ -0,0 +1,189 @@ +// Gale - base thread pool + +package org.galemc.gale.executor; + +import com.mojang.logging.LogUtils; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.thread.BlockableEventLoop; +import net.minecraft.util.thread.ProcessorHandle; +import net.minecraft.util.thread.ReentrantBlockableEventLoop; +import org.galemc.gale.executor.annotation.OriginalServerThreadOnly; +import org.galemc.gale.executor.queue.AnyTickScheduledMainThreadTaskQueue; +import org.galemc.gale.executor.queue.BaseTaskQueues; +import org.galemc.gale.executor.queue.ScheduledMainThreadTaskQueues; +import org.galemc.gale.executor.thread.MainThreadClaim; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.spigotmc.WatchdogThread; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.RejectedExecutionException; +import java.util.function.BooleanSupplier; +import java.util.function.Supplier; + +/** + * This is a base class for {@link MinecraftServer}, as a replacement of {@link BlockableEventLoop} + * (and the intermediary class {@link ReentrantBlockableEventLoop}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public class MinecraftServerBlockableEventLoop implements ProcessorHandle, AbstractBlockableEventLoop { + + private static final String NAME = "Server"; + private static final Logger LOGGER = LogUtils.getLogger(); + + public static volatile int blockingCount; + private static volatile int reentrantCount; + + public static boolean scheduleExecutables() { + return (reentrantCount != 0 || !MainThreadClaim.isCurrentThreadMainThreadAndNotClaimable(Thread.currentThread())) && !MinecraftServer.SERVER.isStopped(); + } + + protected boolean runningTask() { + return reentrantCount != 0; + } + + public CompletableFuture submit(Supplier task) { + return scheduleExecutables() ? CompletableFuture.supplyAsync(task, this) : CompletableFuture.completedFuture(task.get()); + } + + private CompletableFuture submitAsync(Runnable runnable) { + return CompletableFuture.supplyAsync(() -> { + runnable.run(); + return null; + }, this); + } + + public CompletableFuture submit(Runnable task) { + if (scheduleExecutables()) { + return this.submitAsync(task); + } else { + task.run(); + return CompletableFuture.completedFuture(null); + } + } + + public void executeBlocking(Runnable runnable) { + if (!MainThreadClaim.isCurrentThreadMainThreadAndNotClaimable(Thread.currentThread())) { + this.submitAsync(runnable).join(); + } else { + runnable.run(); + } + } + + /** + * @deprecated Use {@link ScheduledMainThreadTaskQueues#add(Runnable, int)} instead: + * do not rely on {@link ScheduledMainThreadTaskQueues#DEFAULT_TASK_MAX_DELAY}. + */ + @Deprecated + @Override + public void tell(@NotNull Runnable message) { + ScheduledMainThreadTaskQueues.add(() -> { + if (Thread.currentThread() != WatchdogThread.instance) { + MinecraftServer.SERVER.getProfiler().incrementCounter("runTask"); + } + ++reentrantCount; + try { + message.run(); + } catch (Exception var3) { + if (var3.getCause() instanceof ThreadDeath) throw var3; // Paper + LOGGER.error(LogUtils.FATAL_MARKER, "Error executing task on {}", NAME, var3); + } finally { + --reentrantCount; + } + MinecraftServer.SERVER.executeMidTickTasks(); // Paper - execute chunk tasks mid tick + }); + } + + @Override + public void execute(@NotNull Runnable var1) { + if (scheduleExecutables()) { + this.tell(var1); + } else { + var1.run(); + } + } + + @Override + public boolean isSameThread() { + return MainThreadClaim.isCurrentThreadMainThreadAndNotClaimable(Thread.currentThread()); + } + + @Override + public void executeIfPossible(Runnable runnable) { + if (MinecraftServer.SERVER.isStopped()) { + throw new RejectedExecutionException("Server already shutting down"); + } else { + this.execute(runnable); + } + } + + /** + * Runs all tasks, regardless of which tick they must be finished in, or whether there is time. + */ + @OriginalServerThreadOnly + protected void runAllMainThreadTasksForAllTicks() { + Runnable task; + while (true) { + // Force polling every tasks regardless of the tick they have to be finished by + MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking = false; + task = ScheduledMainThreadTaskQueues.poll(MinecraftServer.SERVER.originalServerThread, true); + if (task == null) { + break; + } + task.run(); + } + } + + /** + * Runs all tasks while there is time. + * Runs at least all tasks that must be finished in the current tick, regardless of whether there is time. + */ + @OriginalServerThreadOnly + protected void runAllTasksWithinTimeOrForCurrentTick() { + Runnable task; + while (true) { + /* + Update this value accurately: we are in 'spare time' here, we may have more time or not, and we are + definitely not already blocking. + */ + MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking = !MinecraftServer.SERVER.haveTime(); + task = BaseTaskQueues.anyTickScheduledMainThread.poll(MinecraftServer.SERVER.originalServerThread); + if (task == null) { + break; + } + task.run(); + } + } + + /** + * @deprecated Use {@link #managedBlock(BooleanSupplier, Long)} instead. + */ + @OriginalServerThreadOnly + @Deprecated + public void managedBlock(@Nullable BooleanSupplier stopCondition) { + managedBlock(stopCondition, null); + } + + @OriginalServerThreadOnly + public void managedBlock(@Nullable BooleanSupplier stopCondition, @Nullable Long stopTime) { + ++blockingCount; + try { + MainThreadClaim.secondaryThreadsCanStartToClaim(); + try { + MinecraftServer.SERVER.originalServerThread.runTasksUntil(stopCondition, null, stopTime); + } finally { + MainThreadClaim.claimAsOriginalServerThread(); + } + } finally { + --blockingCount; + } + } + + @Override + public @NotNull String name() { + return NAME; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/annotation/PotentiallyYielding.java b/src/main/java/org/galemc/gale/executor/annotation/PotentiallyYielding.java index e87ee2612348fc559b21256cc7cadfc684f01f8e..e7ed376efb811e05b5cfecfa31c9c0041e70cf16 100644 --- a/src/main/java/org/galemc/gale/executor/annotation/PotentiallyYielding.java +++ b/src/main/java/org/galemc/gale/executor/annotation/PotentiallyYielding.java @@ -2,6 +2,9 @@ package org.galemc.gale.executor.annotation; +import org.galemc.gale.executor.lock.YieldingLock; +import org.galemc.gale.executor.thread.BaseThread; + import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Target; @@ -16,6 +19,9 @@ import java.lang.annotation.Target; *
    * In a method annotated with {@link PotentiallyYielding}, the only methods that can be called are those * annotated with {@link PotentiallyYielding} or {@link YieldFree}. + *
    + * It should be assumed that any method annotated with {@link PotentiallyYielding} is potentially blocking if used + * on a thread that is not a {@link BaseThread}. * * @author Martijn Muijsers under AGPL-3.0 */ diff --git a/src/main/java/org/galemc/gale/executor/lock/MultipleWaitingThreadsYieldingLock.java b/src/main/java/org/galemc/gale/executor/lock/MultipleWaitingThreadsYieldingLock.java new file mode 100644 index 0000000000000000000000000000000000000000..cc005d17cd4a3b75cb4dcc809df70ded109ad66c --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/lock/MultipleWaitingThreadsYieldingLock.java @@ -0,0 +1,39 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.lock; + +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.wait.WaitingBaseThreadSet; +import org.galemc.gale.executor.thread.wait.SignalReason; + +import java.util.concurrent.locks.Lock; + +/** + * A {@link YieldingLock} for which multiple base threads may be waiting at the same time. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public class MultipleWaitingThreadsYieldingLock extends YieldingLock { + + private final WaitingBaseThreadSet waitingThreads = new WaitingBaseThreadSet(); + + public MultipleWaitingThreadsYieldingLock(Lock innerLock) { + super(innerLock); + } + + @Override + public void addWaitingThread(BaseThread thread) { + waitingThreads.add(thread); + } + + @Override + public void removeWaitingThread(BaseThread thread) { + waitingThreads.remove(thread); + } + + @Override + public SignalReason getSignalReason() { + return this.waitingThreads; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/lock/YieldingLock.java b/src/main/java/org/galemc/gale/executor/lock/YieldingLock.java new file mode 100644 index 0000000000000000000000000000000000000000..9dafb891aeda688ad668cd429c68d063e48429fe --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/lock/YieldingLock.java @@ -0,0 +1,121 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.lock; + +import org.galemc.gale.executor.annotation.AnyThreadSafe; +import org.galemc.gale.executor.annotation.PotentiallyYielding; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.wait.SignalReason; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; + +/** + * A wrapper for a lock that can be acquired, but if not able to be acquired right away, can cause the current thread + * to perform other tasks, attempting to acquire the lock again at a later time. + *
    + * The lock is reentrant if the underlying controlled lock is. + *
    + * The lock only be speculatively acquired from any {@link BaseThread}. Acquiring it on a thread that is not a + * {@link BaseThread} will perform regular locking on the underlying controlled lock: which typically waits on + * failure, blocking the thread in the process. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@AnyThreadSafe +public abstract class YieldingLock implements Lock { + + private final Lock innerLock; + + public YieldingLock(Lock innerLock) { + this.innerLock = innerLock; + } + + /** + * Attempts to acquire the lock immediately. + * + * @return Whether the lock was acquired. + */ + @YieldFree + @Override + public boolean tryLock() { + return innerLock.tryLock(); + } + + /** + * Acquires the lock. + *
    + * If the current threads is a {@link BaseThread}, this will yield to other tasks while the lock can not be + * acquired. Otherwise, this will block until the lock is acquired. + */ + @PotentiallyYielding + @Override + public void lock() { + // Try to acquire the lock straight away + if (!this.innerLock.tryLock()) { + // If unsuccessful, we find out our current thread + BaseThread baseThread = BaseThread.getCurrent(); + // If we are not on a base thread, we wait for the lock instead of yielding + if (baseThread == null) { + this.innerLock.lock(); + return; + } + // Otherwise, we yield to other tasks until the lock can be acquired + baseThread.yieldUntil(null, this); + } + } + + /** + * Releases the lock (must be called after having completed the computation block that required the lock). + */ + @Override + public void unlock() { + this.innerLock.unlock(); + // Signal the first waiting thread, if any. + // Another thread could also acquire the lock at this moment, so when we signal the thread we obtain below, + // it may already be too late for the polled thread to acquire this lock + // (but note that the same thread cannot have been added again because only the thread itself can do that - + // and it is still waiting). + this.getSignalReason().signalAnother(); + } + + @Override + public boolean tryLock(long l, @NotNull TimeUnit timeUnit) throws InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override + public void lockInterruptibly() throws InterruptedException { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public Condition newCondition() { + // The inner lock may itself not support newCondition and throw UnsupportedOperationException + return this.innerLock.newCondition(); + } + + /** + * Adds a thread to the set of threads waiting for this lock to be released. + * + * @param thread The thread to register as waiting for this lock. + */ + @YieldFree + public abstract void addWaitingThread(BaseThread thread); + + /** + * Removes a thread from the set of threads waiting for this lock to be released. + * + * @param thread The thread to unregister as waiting for this lock. + */ + @YieldFree + public abstract void removeWaitingThread(BaseThread thread); + + @YieldFree + public abstract SignalReason getSignalReason(); + +} diff --git a/src/main/java/org/galemc/gale/executor/queue/AbstractTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/AbstractTaskQueue.java new file mode 100644 index 0000000000000000000000000000000000000000..3a79096e5a16798539d45d252672c3650b7a2afd --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/AbstractTaskQueue.java @@ -0,0 +1,94 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.queue; + +import org.galemc.gale.executor.annotation.AnyThreadSafe; +import org.galemc.gale.executor.annotation.BaseThreadOnly; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.wait.SignalReason; +import org.galemc.gale.executor.thread.wait.TaskWaitingBaseThreads; +import org.jetbrains.annotations.Nullable; + +/** + * An interface for a task queue that may contain tasks that are potentially yielding and tasks that are yield-free. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@AnyThreadSafe @YieldFree +public interface AbstractTaskQueue { + + /** + * @return Whether the tasks in this queue must be executed on the main thread. + */ + boolean isMainThreadOnly(); + + /** + * @return Whether this queue has potentially yielding tasks that could start right now. + * + * @see #hasTasksThatCanStartNow + */ + boolean hasYieldingTasksThatCanStartNow(); + + /** + * @return Whether this queue has yield-free tasks that could start right now. + * + * @see #hasTasksThatCanStartNow + */ + boolean hasFreeTasksThatCanStartNow(); + + /** + * @return Whether this queue has tasks that could start right now. An example of + * tasks that could not start right now are tasks in {@link ScheduledMainThreadTaskQueues} that are scheduled for + * a later tick, while we are already out of spare time this tick. + *
    + * This method does not check whether the given thread is or could claim the main thread: whether a task + * can start now is thread-agnostic and based purely on the state of the queue. + */ + @BaseThreadOnly + default boolean hasTasksThatCanStartNow(BaseThread currentThread) { + return (!currentThread.isRestricted && this.hasYieldingTasksThatCanStartNow()) || this.hasFreeTasksThatCanStartNow(); + } + + /** + * @return Whether this queue has any tasks at all. + */ + boolean hasTasks(); + + /** + * Attempts to poll a task. + *
    + * If the tasks in this queue are main-thread-only or this method may return a main-thread-only task, + * the calling thread must have claimed the main thread and + * {@link org.galemc.gale.executor.thread.MainThreadClaim#canMainThreadBeClaimed} must be false to prevent the + * polling of a main-thread-only task that can then not be immediately started. + * + * @param currentThread The current thread. + * @return The polled task, or null if this queue was empty. + */ + @BaseThreadOnly + @Nullable Runnable poll(BaseThread currentThread); + + /** + * Schedules a new task to this queue. + * + * @param task The task to schedule. + * @param yielding Whether the task is potentially yielding. + */ + void add(Runnable task, boolean yielding); + + /** + * @return The {@link SignalReason} that will be given to {@link TaskWaitingBaseThreads#signal} to signal new + * potentially yielding tasks being added to this queue, or null if irrelevant (e.g. because the signal never + * needs to be repeated, or because this queue will never hold potentially yielding tasks). + */ + @Nullable SignalReason getYieldingSignalReason(); + + /** + * @return The {@link SignalReason} that will be given to {@link TaskWaitingBaseThreads#signal} to signal new + * yield-free tasks being added to this queue, or null if irrelevant (e.g. because the signal never + * needs to be repeated, or because this queue will never hold yield-free tasks). + */ + @Nullable SignalReason getFreeSignalReason(); + +} diff --git a/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledChunkCacheTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledChunkCacheTaskQueue.java new file mode 100644 index 0000000000000000000000000000000000000000..b186e5b7dbd70a2cb3bd2e9559bca791d7ef2fb6 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledChunkCacheTaskQueue.java @@ -0,0 +1,103 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.queue; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import org.galemc.gale.executor.annotation.AnyThreadSafe; +import org.galemc.gale.executor.annotation.MainThreadOnly; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.wait.SignalReason; +import org.galemc.gale.executor.thread.wait.WaitingBaseThreadSet; +import org.jetbrains.annotations.Nullable; + +/** + * This class provides access to, but does not store, the tasks scheduled to be executed on the main thread, + * that are scheduled and normally polled by each world's {@link ServerChunkCache#mainThreadProcessor} in their + * respective {@link ServerChunkCache.MainThreadExecutor#managedBlock}. These tasks could normally also be run in the + * server's {@link MinecraftServer#managedBlock} if there were no more global scheduled main thread tasks, and as + * such we provide access to polling these tasks for any {@link BaseThread} to execute them as the main thread. + *
    + * All tasks provided by this queue must be yield-free. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@AnyThreadSafe @YieldFree +public final class AllLevelsScheduledChunkCacheTaskQueue implements AbstractTaskQueue { + + public static final SignalReason signalReason = SignalReason.createForTaskQueue(true, true, false); + + AllLevelsScheduledChunkCacheTaskQueue() {} + + @Override + public boolean isMainThreadOnly() { + return true; + } + + @Override + public boolean hasYieldingTasksThatCanStartNow() { + return false; + } + + @Override + public boolean hasFreeTasksThatCanStartNow() { + // Skip during server bootstrap or if there is no more time in the current spare time + if (MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking || MinecraftServer.SERVER == null) { + return false; + } + return this.hasTasks(); + } + + @Override + public boolean hasTasks() { + for (ServerLevel worldserver : MinecraftServer.SERVER.getAllLevels()) { + if (worldserver.getChunkSource().mainThreadProcessor.hasPendingTasks()) { + return true; + } + } + return false; + } + + @MainThreadOnly + @Override + public @Nullable Runnable poll(BaseThread currentThread) { + // Skip during server bootstrap or if there is no more time in the current spare time + if (MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking || MinecraftServer.SERVER == null) { + return null; + } + ServerLevel[] levels = MinecraftServer.SERVER.getAllLevelsArray(); + int startIndex = currentThread.allLevelsChunkCacheTaskQueueLevelIterationIndex = Math.min(currentThread.allLevelsChunkCacheTaskQueueLevelIterationIndex, levels.length - 1); + // Paper - force execution of all worlds, do not just bias the first + do { + ServerLevel level = levels[currentThread.allLevelsChunkCacheTaskQueueLevelIterationIndex++]; + if (currentThread.allLevelsChunkCacheTaskQueueLevelIterationIndex == levels.length) { + currentThread.allLevelsChunkCacheTaskQueueLevelIterationIndex = 0; + } + if (level.serverLevelArrayIndex != -1) { + var executor = level.getChunkSource().mainThreadProcessor; + if (executor.hasPendingTasks()) { + return executor::pollTask; + } + } + } while (currentThread.allLevelsChunkCacheTaskQueueLevelIterationIndex != startIndex); + return null; + } + + @Override + public void add(Runnable task, boolean yielding) { + throw new UnsupportedOperationException(); + } + + @Override + public @Nullable SignalReason getYieldingSignalReason() { + return null; + } + + @Override + public SignalReason getFreeSignalReason() { + return signalReason; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledMainThreadChunkTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledMainThreadChunkTaskQueue.java new file mode 100644 index 0000000000000000000000000000000000000000..02b61e420478d5b782930111dad96de5bd9f9801 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledMainThreadChunkTaskQueue.java @@ -0,0 +1,104 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.queue; + +import io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import org.galemc.gale.executor.annotation.AnyThreadSafe; +import org.galemc.gale.executor.annotation.MainThreadOnly; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.wait.SignalReason; +import org.jetbrains.annotations.Nullable; + +/** + * This class provides access to, but does not store, the tasks scheduled to be executed on the main thread, + * that are scheduled and normally polled by each world's {@link ServerLevel#chunkTaskScheduler} using + * respective {@link ChunkTaskScheduler#executeMainThreadTask}. These tasks could normally also be run in the + * server's {@link MinecraftServer#managedBlock} or a level's {@link ServerChunkCache}'s + * {@link ServerChunkCache.MainThreadExecutor#managedBlock}, and as such we provide access to polling these tasks + * for any {@link BaseThread} to execute them as the main thread. + *
    + * All tasks provided by this queue must be yield-free. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@AnyThreadSafe @YieldFree +public final class AllLevelsScheduledMainThreadChunkTaskQueue implements AbstractTaskQueue { + + public static final SignalReason signalReason = SignalReason.createForTaskQueue(true, true, false); + + AllLevelsScheduledMainThreadChunkTaskQueue() {} + + @Override + public boolean isMainThreadOnly() { + return true; + } + + @Override + public boolean hasYieldingTasksThatCanStartNow() { + return false; + } + + @Override + public boolean hasFreeTasksThatCanStartNow() { + // Skip during server bootstrap or if there is no more time in the current spare time + if (MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking || MinecraftServer.SERVER == null) { + return false; + } + return this.hasTasks(); + } + + @Override + public boolean hasTasks() { + for (ServerLevel worldserver : MinecraftServer.SERVER.getAllLevels()) { + if (worldserver.chunkTaskScheduler.mainThreadExecutor.hasScheduledUncompletedTasksVolatile()) { + return true; + } + } + return false; + } + + @MainThreadOnly + @Override + public @Nullable Runnable poll(BaseThread currentThread) { + // Skip during server bootstrap or if there is no more time in the current spare time + if (MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking || MinecraftServer.SERVER == null) { + return null; + } + ServerLevel[] levels = MinecraftServer.SERVER.getAllLevelsArray(); + int startIndex = currentThread.allLevelsScheduledMainThreadChunkTaskQueueLevelIterationIndex = Math.min(currentThread.allLevelsScheduledMainThreadChunkTaskQueueLevelIterationIndex, levels.length - 1); + // Paper - force execution of all worlds, do not just bias the first + do { + ServerLevel level = levels[currentThread.allLevelsScheduledMainThreadChunkTaskQueueLevelIterationIndex++]; + if (currentThread.allLevelsScheduledMainThreadChunkTaskQueueLevelIterationIndex == levels.length) { + currentThread.allLevelsScheduledMainThreadChunkTaskQueueLevelIterationIndex = 0; + } + if (level.serverLevelArrayIndex != -1) { + var executor = level.chunkTaskScheduler.mainThreadExecutor; + if (executor.hasScheduledUncompletedTasksVolatile()) { + return executor::executeTask; + } + } + } while (currentThread.allLevelsScheduledMainThreadChunkTaskQueueLevelIterationIndex != startIndex); + return null; + } + + @Override + public void add(Runnable task, boolean yielding) { + throw new UnsupportedOperationException(); + } + + @Override + public @Nullable SignalReason getYieldingSignalReason() { + return null; + } + + @Override + public SignalReason getFreeSignalReason() { + return signalReason; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/queue/AnyTickScheduledMainThreadTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/AnyTickScheduledMainThreadTaskQueue.java new file mode 100644 index 0000000000000000000000000000000000000000..014c94a590c1a0d534f2009caae3a2e3b4ddcdd6 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/AnyTickScheduledMainThreadTaskQueue.java @@ -0,0 +1,68 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.queue; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import org.galemc.gale.executor.annotation.AnyThreadSafe; +import org.galemc.gale.executor.annotation.MainThreadOnly; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.wait.SignalReason; +import org.jetbrains.annotations.Nullable; + +/** + * This class provides access to, but does not store, the tasks scheduled to be executed on the main thread, + * that must be finished by some time in the future, but not necessarily within the current tick or its spare time. + *
    + * This queue does not support {@link #add}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@AnyThreadSafe @YieldFree +public final class AnyTickScheduledMainThreadTaskQueue implements AbstractTaskQueue { + + AnyTickScheduledMainThreadTaskQueue() {} + + @Override + public boolean isMainThreadOnly() { + return true; + } + + @Override + public boolean hasYieldingTasksThatCanStartNow() { + return ScheduledMainThreadTaskQueues.hasTasksThatCanStartNow(true); + } + + @Override + public boolean hasTasks() { + return ScheduledMainThreadTaskQueues.hasTasks(true); + } + + @Override + public boolean hasFreeTasksThatCanStartNow() { + return false; + } + + @MainThreadOnly + @Override + public @Nullable Runnable poll(BaseThread currentThread) { + return ScheduledMainThreadTaskQueues.poll(currentThread, true); + } + + @Override + public void add(Runnable task, boolean yielding) { + throw new UnsupportedOperationException(); + } + + @Override + public SignalReason getYieldingSignalReason() { + return ScheduledMainThreadTaskQueues.signalReason; + } + + @Override + public @Nullable SignalReason getFreeSignalReason() { + return null; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/queue/BaseTaskQueues.java b/src/main/java/org/galemc/gale/executor/queue/BaseTaskQueues.java new file mode 100644 index 0000000000000000000000000000000000000000..c2756a05c87cf3fe11cec82f4a2453e44fe89595 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/BaseTaskQueues.java @@ -0,0 +1,91 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.queue; + +import org.galemc.gale.executor.thread.BaseThread; + +/** + * This class statically provides a list of task queues containing tasks for {@link BaseThread}s to poll from. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public final class BaseTaskQueues { + + private BaseTaskQueues() {} + + /** + * This queue stores the tasks scheduled to be executed on the main thread, that are procedures that must run + * on the main thread, but their completion is currently needed by another task that has started running on a thread + * that was not the main thread at the time of scheduling the main-thread-only procedure. + *
    + * These tasks are explicitly those that other tasks are waiting on, and as such always have a higher priority + * in being started than pending tasks in represent steps in ticking the server, and as such always have the + * higher priority in being started than pending tasks in {@link TickTaskQueue}, and by extension in + * {@link #anyTickScheduledMainThread} and {@link #scheduledAsync}. + *
    + * This queue may contain potentially yielding and yield-free tasks. + *
    + * This queue's {@link AbstractTaskQueue#add} must not be called from the main thread, because the main thread must + * not defer to itself (because tasks in this queue are assumed to have to run independent of other main thread + * tasks, therefore this will cause a deadlock due to the scheduled task never starting). Instead, any task that + * must be deferred to the main thread must instead simply be executed when encountered on the main thread. + */ + public static final YieldingAndFreeSimpleTaskQueue deferredToMainThread = new YieldingAndFreeSimpleTaskQueue(true, true, true); + + /** + * This queue explicitly stores tasks that represent steps or parts of steps in ticking the server and that must be + * executed on the main thread, and as such always have a higher priority in being started than pending tasks in + * {@link #thisTickScheduledMainThread} and {@link #scheduledAsync}. + *
    + * This queue may contain potentially yielding and yield-free tasks. + *
    + * Tasks in every queue are performed in the order they are added (first-in, first-out). Note that this means + * not all main-thread-only tick tasks are necessarily performed in the order they are added, because they may be + * in different queues: either the queue for potentially yielding tasks or the queue for yield-free tasks. + */ + public static final YieldingAndFreeSimpleTaskQueue mainThreadTick = new YieldingAndFreeSimpleTaskQueue(true, true); + + /** + * Currently unused. + * + * @see ThisTickScheduledMainThreadTaskQueue + */ + public static final ThisTickScheduledMainThreadTaskQueue thisTickScheduledMainThread = new ThisTickScheduledMainThreadTaskQueue(); + + /** + * @see AnyTickScheduledMainThreadTaskQueue + */ + public static final AnyTickScheduledMainThreadTaskQueue anyTickScheduledMainThread = new AnyTickScheduledMainThreadTaskQueue(); + + /** + * This queue explicitly stores tasks that represent steps or parts of steps in ticking the server that do not have + * to be executed on the main thread (but must be executed on a {@link BaseThread}), and have a higher priority + * in being started than pending tasks in {@link #scheduledAsync}. + *
    + * This queue may contain potentially yielding and yield-free tasks. + *
    + * Tasks in every queue are performed in the order they are added (first-in, first-out). Note that this means + * not all {@link BaseThread} tick tasks are necessarily performed in the order they are added, because they may be + * in different queues: either the queue for potentially yielding tasks or the queue for yield-free tasks. + */ + public static final YieldingAndFreeSimpleTaskQueue baseThreadTick = new YieldingAndFreeSimpleTaskQueue(false, true); + + /** + * @see AllLevelsScheduledChunkCacheTaskQueue + */ + public static final AllLevelsScheduledChunkCacheTaskQueue allLevelsScheduledChunkCache = new AllLevelsScheduledChunkCacheTaskQueue(); + + /** + * @see AllLevelsScheduledMainThreadChunkTaskQueue + */ + public static final AllLevelsScheduledMainThreadChunkTaskQueue allLevelsScheduledMainThreadChunk = new AllLevelsScheduledMainThreadChunkTaskQueue(); + + /** + * This queue stores the tasks scheduled to be executed on any thread, which would usually be stored in various + * executors with a specific purpose. + *
    + * This queue may contain potentially yielding and yield-free tasks. + */ + public static final YieldingAndFreeSimpleTaskQueue scheduledAsync = new YieldingAndFreeSimpleTaskQueue(false,false); + +} diff --git a/src/main/java/org/galemc/gale/executor/queue/FreeSimpleTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/FreeSimpleTaskQueue.java new file mode 100644 index 0000000000000000000000000000000000000000..01017d6f392ccb5172b4fa4522576ffb50ef06a3 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/FreeSimpleTaskQueue.java @@ -0,0 +1,53 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.queue; + +import org.galemc.gale.concurrent.UnterminableExecutorService; +import org.galemc.gale.executor.annotation.AnyThreadSafe; +import org.galemc.gale.executor.annotation.BaseThreadOnly; +import org.galemc.gale.executor.annotation.YieldFree; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; + +/** + * A base class for a task queue that contains tasks that are all yield-free. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@BaseThreadOnly @YieldFree +public class FreeSimpleTaskQueue extends SimpleTaskQueue { + + FreeSimpleTaskQueue(boolean mainThreadOnly, boolean baseThreadOnly) { + super(mainThreadOnly, baseThreadOnly, false, true); + } + + FreeSimpleTaskQueue(boolean mainThreadOnly, boolean baseThreadOnly, boolean lifoQueues) { + super(mainThreadOnly, baseThreadOnly, false, true, lifoQueues); + } + + /** + * Schedules a new task to this queue. + * + * @param task The task to schedule. + */ + @AnyThreadSafe @YieldFree + public void add(Runnable task) { + this.add(task, false); + } + + /** + * An executor for adding tasks to this queue, + * where {@link Executor#execute} calls {@link #add}. + */ + public final ExecutorService executor = new UnterminableExecutorService() { + + @Override + public void execute(@NotNull Runnable runnable) { + add(runnable, false); + } + + }; + +} diff --git a/src/main/java/org/galemc/gale/executor/queue/ScheduledMainThreadTaskQueues.java b/src/main/java/org/galemc/gale/executor/queue/ScheduledMainThreadTaskQueues.java new file mode 100644 index 0000000000000000000000000000000000000000..896f84decd11c6afdcd3f956983215d4e22ba0b7 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/ScheduledMainThreadTaskQueues.java @@ -0,0 +1,294 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.queue; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.thread.BlockableEventLoop; +import org.galemc.gale.concurrent.UnterminableExecutorService; +import org.galemc.gale.executor.annotation.Access; +import org.galemc.gale.executor.annotation.AnyThreadSafe; +import org.galemc.gale.executor.annotation.Guarded; +import org.galemc.gale.executor.annotation.MainThreadOnly; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.wait.SignalReason; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Queue; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * This class stores the tasks scheduled to be executed on the main thread, which would usually be stored in a queue + * in the {@link MinecraftServer} in its role as a {@link BlockableEventLoop}. These tasks are not steps of + * ticking, but other tasks that must be executed at some point while no other main-thread-only task is running. + *
    + * Each task is stored by the number of ticks left before it has to be executed. Tasks will always definitely be + * executed within the required number of ticks. Tasks may also be executed earlier than needed + * if there is time to do so. + *
    + * Note that this means not all tasks are necessarily performed in the order they are added, because they may be in + * different queues based on the number of ticks before they have to be executed. + *
    + * All contained tasks are currently assumed to be potentially yielding: no special distinction for yield-free + * tasks is made. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@AnyThreadSafe @YieldFree +public final class ScheduledMainThreadTaskQueues { + + ScheduledMainThreadTaskQueues() {} + + public static int DEFAULT_TASK_MAX_DELAY = 2; + public static int COMPLETE_CHUNK_FUTURE_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY; + public static int POST_CHUNK_LOAD_JOIN_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY; + public static int ANTI_XRAY_MODIFY_BLOCKS_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY; + public static int TELEPORT_ASYNC_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY; + public static int SEND_COMMAND_COMPLETION_SUGGESTIONS_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY; + public static int KICK_FOR_COMMAND_PACKET_SPAM_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY; + public static int KICK_FOR_RECIPE_PACKET_SPAM_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY; + public static int KICK_FOR_BOOK_TOO_LARGE_PACKET_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY; + public static int KICK_FOR_EDITING_BOOK_TOO_QUICKLY_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY; + public static int KICK_FOR_ILLEGAL_CHARACTERS_IN_CHAT_PACKET_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY; + public static int KICK_FOR_OUT_OF_ORDER_CHAT_PACKET_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY; + public static int HANDLE_DISCONNECT_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY; + + /** + * A number of queues, with the queue at index i being the queue to be used after another i ticks pass + * even when {@link MinecraftServer#haveTime()} is false. + */ + @Guarded(value = "#lock", except = "when optimistically reading using versionStamp as a stamp") + public static Queue[] queues = { new MultiThreadedQueue<>() }; + + /** + * Probably the lowest index of any queue in {@link #queues} that is non-empty. + * This is maintained as well as possible. + */ + public static volatile int firstQueueWithPotentialTasksIndex = 0; + + @Guarded(value = "#lock", fieldAccess = Access.WRITE) + public static volatile long versionStamp = 0; + + private static final ReadWriteLock lock = new ReentrantReadWriteLock(); + private static final Lock readLock = lock.readLock(); + public static final Lock writeLock = lock.writeLock(); + + static final SignalReason signalReason = SignalReason.createForTaskQueue(true, true, true); + + /** + * @return Whether there are any scheduled main thread tasks that could start right now. Tasks that are scheduled + * for a later tick will only be regarded as able to be started if both we are not out of spare time this tick + * and {@code tryNonCurrentTickQueuesAtAll} is true. + *
    + * This method does not check whether the given thread is or could claim the main thread: whether a task + * can start now is thread-agnostic and based purely on the state of the queue. + */ + public static boolean hasTasksThatCanStartNow(boolean tryNonCurrentTickQueuesAtAll) { + return hasTasks(tryNonCurrentTickQueuesAtAll, true); + } + + /** + * @return Whether there are any scheduled main thread tasks, not counting any tasks that do not have to be + * finished within the current tick if {@code tryNonCurrentTickQueuesAtAll} is false. + */ + public static boolean hasTasks(boolean tryNonCurrentTickQueuesAtAll) { + return hasTasks(tryNonCurrentTickQueuesAtAll, false); + } + + /** + * Common implementation for {@link #hasTasksThatCanStartNow} and {@link #hasTasks(boolean)}. + */ + private static boolean hasTasks(boolean tryNonCurrentTickQueuesAtAll, boolean checkCanStartNow) { + //noinspection StatementWithEmptyBody + while (!readLock.tryLock()); + try { + // Try the queue most likely to contain tasks first + if (firstQueueWithPotentialTasksIndex == 0 || (tryNonCurrentTickQueuesAtAll && (!checkCanStartNow || !MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking))) { + if (!queues[firstQueueWithPotentialTasksIndex].isEmpty()) { + return true; + } + } + int checkUpTo = tryNonCurrentTickQueuesAtAll && (!checkCanStartNow || !MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking) ? queues.length : 1; + for (int i = 0; i < checkUpTo; i++) { + if (!queues[i].isEmpty()) { + return true; + } + } + } finally { + readLock.unlock(); + } + return false; + } + + /** + * Polls a task from this queue and returns it. + * Tasks that are scheduled for a later tick will only be regarded as able to be started if both we are not out of + * spare time this tick and {@code tryNonCurrentTickQueuesAtAll} is true. + *
    + * This method does not check whether the given thread is or could claim the main thread: whether a task + * can start now is thread-agnostic and based purely on the state of the queue. + * + * @return The task that was polled, or null if no task was found. + */ + @MainThreadOnly + public static @Nullable Runnable poll(BaseThread currentThread, boolean tryNonCurrentTickQueuesAtAll) { + // Since we assume the tasks in this queue to be potentially yielding, fail if the thread is restricted + if (currentThread.isRestricted) { + return null; + } + pollFromFirstQueueOrOthers: while (true) { + // Try to get a task from the first queue first + Object task = queues[0].poll(); + if (task != null) { + return (Runnable) task; + } else if (!tryNonCurrentTickQueuesAtAll || MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking) { + // We needed a task from the first queue + if (queues[0].isEmpty()) { + // The first queue really is empty, so we fail + return null; + } + // An element was added to the first queue in the meantime, try again + continue; + } + tryGetRunnableUntilSuccessOrNothingChanged: while (true) { + boolean goOverAllQueues = firstQueueWithPotentialTasksIndex == 0; + int oldFirstQueueWithElementsIndex = firstQueueWithPotentialTasksIndex; + long oldVersionStamp = versionStamp; + for (int i = goOverAllQueues ? 0 : firstQueueWithPotentialTasksIndex; i < queues.length; i++) { + if (!queues[i].isEmpty()) { + if (i == 0 || !MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking) { + task = queues[i].poll(); + if (task == null) { + // We lost a race condition between the isEmpty() and poll() calls: just try again + continue tryGetRunnableUntilSuccessOrNothingChanged; + } + return (Runnable) task; + } + // Apparently we must now poll from the first queue only, try again + continue pollFromFirstQueueOrOthers; + } + // The queue was empty, the first queue with potential tasks must be the next one + if (i == firstQueueWithPotentialTasksIndex) { + if (i == queues.length - 1) { + firstQueueWithPotentialTasksIndex = 0; + } else { + firstQueueWithPotentialTasksIndex = i + 1; + } + } + } + if (goOverAllQueues && firstQueueWithPotentialTasksIndex == 0 && oldVersionStamp == versionStamp) { + /* + We went over all queues and nothing changed in the meantime, + we give up, there appear to be no more tasks. + */ + return null; + } + // Something changed, or we did not go over all queues, try again + } + } + } + + public static int getTaskCount() { + //noinspection StatementWithEmptyBody + while (!readLock.tryLock()); + try { + int count = 0; + for (Queue queue : queues) { + count += queue.size(); + } + return count; + } finally { + readLock.unlock(); + } + } + + /** + * Schedules a new task to this queue. + * + * @param task The task to schedule. + * + * @deprecated Use {@link #add(Runnable, int)} instead: do not rely on {@link #DEFAULT_TASK_MAX_DELAY}. + */ + @Deprecated + public static void add(Runnable task) { + add(task, DEFAULT_TASK_MAX_DELAY); + } + + /** + * Schedules a new task to this queue. + * + * @param task The task to schedule. + * @param maxDelay The maximum number of ticks that the task must be finished by. + * A value of 0 means the task must be finished before the end of the current tick. + */ + public static void add(Runnable task, int maxDelay) { + // Paper start - anything that does try to post to main during watchdog crash, run on watchdog + if (MinecraftServer.SERVER.hasStopped && Thread.currentThread() == MinecraftServer.SERVER.shutdownThread) { + task.run(); + return; + } + // Paper end - anything that does try to post to main during watchdog crash, run on watchdog + //noinspection StatementWithEmptyBody + while (!readLock.tryLock()); + try { + versionStamp++; + queues[maxDelay].add(task); + if (maxDelay < firstQueueWithPotentialTasksIndex) { + firstQueueWithPotentialTasksIndex = maxDelay; + } +// LockSupport.unpark(MinecraftServer.SERVER.originalServerThread); + versionStamp++; + } finally { + readLock.unlock(); + } + MinecraftServer.nextTimeAssumeWeMayHaveDelayedTasks = true; + signalReason.signalAnother(); + } + + /** + * This moves the queues in {@link #queues}. + */ + public static void shiftTasksForNextTick() { + //noinspection StatementWithEmptyBody + while (!writeLock.tryLock()); + try { + versionStamp++; + // Move the queues to the preceding position + Queue firstQueue = queues[0]; + for (int i = 1; i < queues.length; i++) { + queues[i - 1] = queues[i]; + } + // Re-use the same instance that was the old first queue as the new last queue + queues[queues.length - 1] = firstQueue; + // Move any elements that were still present in the previous first queue to the new first queue + queues[0].addAll(firstQueue); + firstQueue.clear(); + versionStamp++; + } finally { + writeLock.unlock(); + } + } + + /** + * An executor for adding tasks to this queue, + * where {@link Executor#execute} calls {@link #add}. + * + * @deprecated Use {@link #add(Runnable, int)} instead: do not rely on {@link #DEFAULT_TASK_MAX_DELAY}. + */ + @Deprecated + public static final ExecutorService executor = new UnterminableExecutorService() { + + @Override + public void execute(@NotNull Runnable runnable) { + add(runnable); + } + + }; + +} diff --git a/src/main/java/org/galemc/gale/executor/queue/SimpleTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/SimpleTaskQueue.java new file mode 100644 index 0000000000000000000000000000000000000000..bcc14c165011af4dae98769f5dc48a16c676f205 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/SimpleTaskQueue.java @@ -0,0 +1,185 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.queue; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import net.minecraft.server.MinecraftServer; +import org.galemc.gale.collection.FIFOConcurrentLinkedQueue; +import org.galemc.gale.concurrent.UnterminableExecutorService; +import org.galemc.gale.executor.annotation.AnyThreadSafe; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.MainThreadClaim; +import org.galemc.gale.executor.thread.SecondaryThread; +import org.galemc.gale.executor.thread.SecondaryThreadPool; +import org.galemc.gale.executor.thread.wait.SignalReason; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Queue; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; + +/** + * A base class for a task queue that may contain tasks that are potentially yielding and tasks that are yield-free. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@AnyThreadSafe @YieldFree +public class SimpleTaskQueue implements AbstractTaskQueue { + + /** + * Whether the tasks in this queue can only be performed on the main thread. + */ + public final boolean mainThreadOnly; + + /** + * Whether the tasks in this queue can only be performed on a {@link BaseThread}, + * as opposed to being able to be performed on any thread. + */ + public final boolean baseThreadOnly; + + /** + * Whether tasks in this queue can be potentially yielding. + */ + public final boolean canHaveYieldingTasks; + + /** + * Whether tasks in this queue can be yield-free. + */ + public final boolean canHaveFreeTasks; + + /** + * The queue of potentially yielding tasks, or null if {@link #canHaveYieldingTasks} is false. + */ + private @Nullable Queue yieldingQueue; + + /** + * The queue of yield-free tasks, or null if {@link #canHaveFreeTasks} is false. + */ + private @Nullable Queue freeQueue; + + /** + * The {@link SignalReason} used when signalling threads due to newly added potentially yielding tasks, + * or null if {@link #canHaveYieldingTasks} is false. + */ + private final @Nullable SignalReason yieldingSignalReason; + + /** + * The {@link SignalReason} used when signalling threads due to newly added yield-free tasks, + * or null if {@link #canHaveFreeTasks} is false. + */ + private final @Nullable SignalReason freeSignalReason; + + SimpleTaskQueue(boolean mainThreadOnly, boolean baseThreadOnly, boolean canHaveYieldingTasks, boolean canHaveFreeTasks) { + this(mainThreadOnly, baseThreadOnly, canHaveYieldingTasks, canHaveFreeTasks, false); + } + + /** + * @param mainThreadOnly Value for {@link #mainThreadOnly}. + * @param baseThreadOnly Value for {@link #baseThreadOnly}. + * @param canHaveYieldingTasks Value for {@link #canHaveYieldingTasks}. + * @param canHaveFreeTasks Value for {@link #canHaveFreeTasks}. + * @param lifoQueues If true, the {@link #yieldingQueue} and {@link #freeQueue} will be LIFO; + * otherwise, they will be FIFO. + */ + SimpleTaskQueue(boolean mainThreadOnly, boolean baseThreadOnly, boolean canHaveYieldingTasks, boolean canHaveFreeTasks, boolean lifoQueues) { + if (mainThreadOnly && !baseThreadOnly) { + throw new IllegalArgumentException(); + } + this.mainThreadOnly = mainThreadOnly; + this.baseThreadOnly = baseThreadOnly; + this.canHaveYieldingTasks = canHaveYieldingTasks; + this.canHaveFreeTasks = canHaveFreeTasks; + this.yieldingQueue = this.canHaveYieldingTasks ? (lifoQueues ? new FIFOConcurrentLinkedQueue<>() : new MultiThreadedQueue<>()) : null; + this.freeQueue = this.canHaveFreeTasks ? (lifoQueues ? new FIFOConcurrentLinkedQueue<>() : new MultiThreadedQueue<>()) : null; + this.yieldingSignalReason = SignalReason.createForTaskQueue(this.mainThreadOnly, this.baseThreadOnly, true); + this.freeSignalReason = SignalReason.createForTaskQueue(this.mainThreadOnly, this.baseThreadOnly, false); + } + + @Override + public boolean isMainThreadOnly() { + return this.mainThreadOnly; + } + + @Override + public boolean hasYieldingTasksThatCanStartNow() { + return this.canHaveYieldingTasks && !this.yieldingQueue.isEmpty(); + } + + @Override + public boolean hasFreeTasksThatCanStartNow() { + return this.canHaveFreeTasks && !this.freeQueue.isEmpty(); + } + + @Override + public boolean hasTasks() { + return (this.canHaveYieldingTasks && !this.yieldingQueue.isEmpty()) || (this.canHaveFreeTasks && !this.freeQueue.isEmpty()); + } + + /** + * Attempts to poll a task. + * This always returns null on the {@link MinecraftServer#originalServerThread} if + * {@link MinecraftServer#canPollAsyncTasksOnOriginalServerThread} is false and {@link #baseThreadOnly} is false, + * and always returns null on a {@link SecondaryThread} if + * {@link SecondaryThreadPool#canOnlyPollAsyncTasksOnSecondaryThreads} is true and {@link #baseThreadOnly} is true. + * + * @see AbstractTaskQueue#poll + */ + @Override + public @Nullable Runnable poll(BaseThread currentThread) { + boolean isOriginalServerThread = MinecraftServer.SERVER == null || currentThread == MinecraftServer.SERVER.originalServerThread; + if (isOriginalServerThread && !this.baseThreadOnly && !MinecraftServer.canPollAsyncTasksOnOriginalServerThread) { + return null; + } else if (!isOriginalServerThread && this.baseThreadOnly && SecondaryThreadPool.canOnlyPollAsyncTasksOnSecondaryThreads) { + return null; + } + Runnable task; + if (!currentThread.isRestricted && this.canHaveYieldingTasks) { + task = this.yieldingQueue.poll(); + if (task != null) { + /* + If the thread was woken up for a different reason, signal for the same reason to wake up another + potential thread waiting for the reason. + */ + if (currentThread.lastSignalReason != null && currentThread.lastSignalReason != this.yieldingSignalReason) { + currentThread.lastSignalReason.signalAnother(); + } + return task; + } + } + if (this.canHaveFreeTasks) { + task = this.freeQueue.poll(); + if (task != null) { + /* + If the thread was woken up for a different reason, signal for the same reason to wake up another + potential thread waiting for the reason. + */ + if (currentThread.lastSignalReason != null && currentThread.lastSignalReason != this.freeSignalReason) { + currentThread.lastSignalReason.signalAnother(); + } + return task; + } + } + return null; + } + + @Override + public void add(Runnable task, boolean yielding) { + (yielding ? this.yieldingQueue : this.freeQueue).add(task); + if (!this.mainThreadOnly || MainThreadClaim.canMainThreadBeClaimed) { + (yielding ? this.yieldingSignalReason : this.freeSignalReason).signalAnother(); + } + } + + @Override + public @Nullable SignalReason getYieldingSignalReason() { + return this.yieldingSignalReason; + } + + @Override + public @Nullable SignalReason getFreeSignalReason() { + return this.freeSignalReason; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/queue/ThisTickScheduledMainThreadTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/ThisTickScheduledMainThreadTaskQueue.java new file mode 100644 index 0000000000000000000000000000000000000000..b94bbeb65d232d8015fd5d76c004145db49bc07a --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/ThisTickScheduledMainThreadTaskQueue.java @@ -0,0 +1,82 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.queue; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.thread.BlockableEventLoop; +import org.galemc.gale.concurrent.UnterminableExecutorService; +import org.galemc.gale.executor.annotation.Access; +import org.galemc.gale.executor.annotation.AnyThreadSafe; +import org.galemc.gale.executor.annotation.Guarded; +import org.galemc.gale.executor.annotation.MainThreadOnly; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.wait.SignalReason; +import org.galemc.gale.executor.thread.wait.TaskWaitingBaseThreads; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Queue; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.LockSupport; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * This class provides access to, but does not store, the tasks scheduled to be executed on the main thread, + * that must be finished in the current tick or its spare time. + *
    + * This queue does not support {@link #add}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@AnyThreadSafe @YieldFree +public final class ThisTickScheduledMainThreadTaskQueue implements AbstractTaskQueue { + + ThisTickScheduledMainThreadTaskQueue() {} + + @Override + public boolean isMainThreadOnly() { + return true; + } + + @Override + public boolean hasYieldingTasksThatCanStartNow() { + return ScheduledMainThreadTaskQueues.hasTasksThatCanStartNow(false); + } + + @Override + public boolean hasTasks() { + return ScheduledMainThreadTaskQueues.hasTasks(false); + } + + @Override + public boolean hasFreeTasksThatCanStartNow() { + return false; + } + + @MainThreadOnly + @Override + public @Nullable Runnable poll(BaseThread currentThread) { + return ScheduledMainThreadTaskQueues.poll(currentThread, false); + } + + @Override + public void add(Runnable task, boolean yielding) { + throw new UnsupportedOperationException(); + } + + @Override + public SignalReason getYieldingSignalReason() { + return ScheduledMainThreadTaskQueues.signalReason; + } + + @Override + public @Nullable SignalReason getFreeSignalReason() { + return null; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/queue/TickTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/TickTaskQueue.java new file mode 100644 index 0000000000000000000000000000000000000000..ee16e3b71c7951062596fef316d27bc66a3ad655 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/TickTaskQueue.java @@ -0,0 +1,281 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.queue; + +import net.minecraft.server.MinecraftServer; +import org.galemc.gale.executor.annotation.AnyThreadSafe; +import org.galemc.gale.executor.annotation.MainThreadOnly; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.MainThreadClaim; +import org.galemc.gale.executor.thread.SecondaryThread; +import org.galemc.gale.executor.thread.wait.SignalReason; +import org.galemc.gale.executor.thread.wait.WaitingBaseThreadSet; +import org.jetbrains.annotations.Nullable; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * This class stores the tasks scheduled to be executed on any thread that can participate in ticking, + * which is the {@link MinecraftServer#originalServerThread} and any {@link SecondaryThread}. + *
    + * These tasks are explicitly those that represent steps in ticking the server, and as such always have a + * higher priority in being started than pending tasks in + * {@link ScheduledMainThreadTaskQueues} and {@link ScheduledAsyncTaskQueue}. + *
    + * This executor stores four queues, for tasks that: + *
      + *
    • Must be performed on the main thread, and are potentially yielding
    • + *
    • Must be performed on the main thread, and are yield-free
    • + *
    • Can be performed on any base thread, an are potentially yielding
    • + *
    • Can be performed on any base thread, and are yield-free
    • + *
    + * Tasks in every queue are performed in the order they are added (first-in, first-out). Note that this means + * not all main-thread-only tasks are necessarily performed in the order they are added, because they may be in + * different queues. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@AnyThreadSafe @YieldFree +public final class TickTaskQueue { + + TickTaskQueue() {} + + /** + * The queue of potentially yielding main-thread-only tasks. + */ + private static final Queue mainThreadYieldingQueue = new ConcurrentLinkedQueue<>(); + + /** + * The queue of yield-free main-thread-only tasks. + */ + private static final Queue mainThreadFreeQueue = new ConcurrentLinkedQueue<>(); + + /** + * The queue of potentially yielding {@link BaseThread} tasks. + */ + private static final Queue baseThreadYieldingQueue = new ConcurrentLinkedQueue<>(); + + /** + * The queue of yield-free {@link BaseThread} tasks. + */ + private static final Queue baseThreadFreeQueue = new ConcurrentLinkedQueue<>(); + + /** + * The base threads waiting for yield-free tasks to be added. + */ + private static final WaitingBaseThreadSet freeWaitingThreads = new WaitingBaseThreadSet() { + + @Override + public boolean pollAndSignal(SignalReason reason) { + if (super.pollAndSignal(reason)) { + return true; + } + return waitingThreads.pollAndSignal(reason); + } + + }; + + /** + * The base threads waiting for tasks to be added. + */ + private static final WaitingBaseThreadSet waitingThreads = new WaitingBaseThreadSet(); + + private static final SignalReason mainThreadOnlyFreeOrMoreTaskSignalReason = new SignalReason() { + + @Override + public boolean signalAnother() { + if (MainThreadClaim.canMainThreadBeClaimed) { + if (freeWaitingThreads.pollAndSignal(this)) { + return true; + } + if (waitingThreads.pollAndSignal(this)) { + return true; + } + } + return false; + } + + }; + + private static final SignalReason generalMainThreadOnlyTaskSignalReason = new SignalReason() { + + @Override + public boolean signalAnother() { + if (MainThreadClaim.canMainThreadBeClaimed) { + if (waitingThreads.pollAndSignal(this)) { + return true; + } + } + return false; + } + + }; + + /** + * Attempts to poll a yield-free main-thread-only task. + */ + @MainThreadOnly + public static @Nullable Runnable pollMainThreadOnlyFreeTaskAsMainThread() { + return mainThreadFreeQueue.poll(); + } + + /** + * Attempts to poll a main-thread-only task. + * The different types of tasks are attempted to be polled in the following order: + *
      + *
    1. Potentially yielding tasks
    2. + *
    3. Yield-free tasks
    4. + *
    + */ + @MainThreadOnly + public static @Nullable Runnable pollMainThreadOnlyTaskAsMainThread() { + Runnable task = mainThreadYieldingQueue.poll(); + if (task != null) { + return task; + } + return mainThreadFreeQueue.poll(); + } + + /** + * Attempts to poll a yield-free that can be performed on any {@link BaseThread}. + */ + public static @Nullable Runnable pollNonMainThreadOnlyFreeTask() { + return baseThreadFreeQueue.poll(); + } + + /** + * Attempts to poll a task that can be performed on any {@link BaseThread}. + * The different types of tasks are attempted to be polled in the following order: + *
      + *
    1. Potentially yielding tasks
    2. + *
    3. Yield-free tasks
    4. + *
    + */ + public static @Nullable Runnable pollNonMainThreadOnlyTask() { + Runnable task = baseThreadYieldingQueue.poll(); + if (task != null) { + return task; + } + return baseThreadFreeQueue.poll(); + } + + /** + * @return Whether there are pending potentially yielding main-thread-only tasks. + */ + public static boolean hasMainThreadOnlyYieldingTasks() { + return !mainThreadYieldingQueue.isEmpty(); + } + + /** + * @return Whether there are pending potentially yielding {@link BaseThread} tasks. + */ + public static boolean hasBaseThreadYieldingTasks() { + return !baseThreadYieldingQueue.isEmpty(); + } + + /** + * @return Whether there are pending yield-free tasks that must be performed on the main thread. + */ + public static boolean hasMainThreadOnlyFreeTasks() { + return !mainThreadFreeQueue.isEmpty(); + } + + /** + * @return Whether there are pending main-thread-only tasks. + */ + public static boolean hasMainThreadOnlyTasks() { + return !mainThreadYieldingQueue.isEmpty() || !mainThreadFreeQueue.isEmpty(); + } + + /** + * @return Whether there are pending yield-free non-main-thread-only tasks. + */ + public static boolean hasNonMainThreadOnlyFreeTasks() { + return !baseThreadFreeQueue.isEmpty(); + } + + /** + * @return Whether there are pending non-main-thread-only. + */ + public static boolean hasNonMainThreadOnlyTasks() { + return !baseThreadYieldingQueue.isEmpty() || !baseThreadFreeQueue.isEmpty(); + } + + /** + * @return Whether there are pending tasks. + */ + public static boolean hasTasks() { + return !mainThreadYieldingQueue.isEmpty() || !mainThreadFreeQueue.isEmpty() || !baseThreadYieldingQueue.isEmpty() || !baseThreadFreeQueue.isEmpty(); + } + + /** + * Schedules a new task to this queue. + * + * @param task The task to schedule. + * @param mainThreadOnly Whether the task must be performed on the main thread. + * @param yielding Whether the task is potentially yielding. + */ + public static void add(Runnable task, boolean mainThreadOnly, boolean yielding) { + (mainThreadOnly ? (yielding ? mainThreadYieldingQueue : mainThreadFreeQueue) : (yielding ? baseThreadYieldingQueue : baseThreadFreeQueue)).add(task); + if (!mainThreadOnly || MainThreadClaim.canMainThreadBeClaimed) { + if (!yielding) { + if (freeWaitingThreads.pollAndSignal(mainThreadOnly ? mainThreadOnlyFreeOrMoreTaskSignalReason : freeWaitingThreads)) { + return; + } + } + waitingThreads.pollAndSignal(mainThreadOnly ? generalMainThreadOnlyTaskSignalReason : waitingThreads); + } + } + + public static void signalWaitingThreadsAfterMainThreadBecameClaimable() { + if (!mainThreadFreeQueue.isEmpty()) { + if (freeWaitingThreads.pollAndSignal()) { + return; + } + waitingThreads.pollAndSignal(); + return; + } + if (!mainThreadYieldingQueue.isEmpty()) { + waitingThreads.pollAndSignal(); + } + } + + /** + * Adds a thread to the set of threads waiting for certain new tasks to be added to this queue. + * + * @param thread The thread to register as waiting for new tasks. + * @param isRestricted Whether the given thread is restricted. + */ + public static void addWaitingThread(BaseThread thread, boolean isRestricted) { + (isRestricted ? freeWaitingThreads : waitingThreads).add(thread); + } + + /** + * Removes a thread from the set of threads waiting for certain new tasks to be added to this queue. + * + * @param thread The thread to unregister as waiting for new tasks. + * @param isRestricted Whether the given thread was restricted when being added. + */ + public static void removeWaitingThread(BaseThread thread, boolean isRestricted) { + (isRestricted ? freeWaitingThreads : waitingThreads).remove(thread); + } + + public static boolean isNonMainThreadOnlyFreeOrMoreTaskSignalReason(SignalReason reason) { + return reason == freeWaitingThreads || reason == waitingThreads; + } + + public static boolean isGeneralNonMainThreadOnlyTaskSignalReason(SignalReason reason) { + return reason == waitingThreads; + } + + public static boolean isMainThreadOnlyFreeOrMoreTaskSignalReason(SignalReason reason) { + return reason == mainThreadOnlyFreeOrMoreTaskSignalReason || reason == generalMainThreadOnlyTaskSignalReason; + } + + public static boolean isGeneralMainThreadOnlyTaskSignalReason(SignalReason reason) { + return reason == generalMainThreadOnlyTaskSignalReason; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/queue/YieldingAndFreeSimpleTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/YieldingAndFreeSimpleTaskQueue.java new file mode 100644 index 0000000000000000000000000000000000000000..7078d50a575f875828ce8284807f2545b0ca3b0d --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/YieldingAndFreeSimpleTaskQueue.java @@ -0,0 +1,56 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.queue; + +import org.galemc.gale.concurrent.UnterminableExecutorService; +import org.galemc.gale.executor.annotation.BaseThreadOnly; +import org.galemc.gale.executor.annotation.YieldFree; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; + +/** + * A base class for a task queue that contains tasks that are potentially yielding and tasks that are + * yield-free. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@BaseThreadOnly @YieldFree +public class YieldingAndFreeSimpleTaskQueue extends SimpleTaskQueue { + + YieldingAndFreeSimpleTaskQueue(boolean mainThreadOnly, boolean baseThreadOnly) { + super(mainThreadOnly, baseThreadOnly, true, true); + } + + YieldingAndFreeSimpleTaskQueue(boolean mainThreadOnly, boolean baseThreadOnly, boolean lifoQueues) { + super(mainThreadOnly, baseThreadOnly, true, true, lifoQueues); + } + + /** + * An executor for adding potentially yielding tasks to this queue, + * where {@link Executor#execute} calls {@link #add}. + */ + public final ExecutorService yieldingExecutor = new UnterminableExecutorService() { + + @Override + public void execute(@NotNull Runnable runnable) { + add(runnable, true); + } + + }; + + /** + * An executor for adding yield-free tasks to this queue, + * where {@link Executor#execute} calls {@link #add}. + */ + public final ExecutorService freeExecutor = new UnterminableExecutorService() { + + @Override + public void execute(@NotNull Runnable runnable) { + add(runnable, false); + } + + }; + +} diff --git a/src/main/java/org/galemc/gale/executor/queue/YieldingSimpleTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/YieldingSimpleTaskQueue.java new file mode 100644 index 0000000000000000000000000000000000000000..e79f426883c77f81c7068c5d019f1708ef0de5f9 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/YieldingSimpleTaskQueue.java @@ -0,0 +1,53 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.queue; + +import org.galemc.gale.concurrent.UnterminableExecutorService; +import org.galemc.gale.executor.annotation.AnyThreadSafe; +import org.galemc.gale.executor.annotation.BaseThreadOnly; +import org.galemc.gale.executor.annotation.YieldFree; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; + +/** + * A base class for a task queue that contains tasks that are all potentially yielding. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@BaseThreadOnly @YieldFree +public class YieldingSimpleTaskQueue extends SimpleTaskQueue { + + YieldingSimpleTaskQueue(boolean mainThreadOnly, boolean baseThreadOnly) { + super(mainThreadOnly, baseThreadOnly, true, false); + } + + YieldingSimpleTaskQueue(boolean mainThreadOnly, boolean baseThreadOnly, boolean lifoQueues) { + super(mainThreadOnly, baseThreadOnly, true, false, lifoQueues); + } + + /** + * Schedules a new task to this queue. + * + * @param task The task to schedule. + */ + @AnyThreadSafe @YieldFree + public void add(Runnable task) { + this.add(task, true); + } + + /** + * An executor for adding tasks to this queue, + * where {@link Executor#execute} calls {@link #add}. + */ + public final ExecutorService executor = new UnterminableExecutorService() { + + @Override + public void execute(@NotNull Runnable runnable) { + add(runnable, true); + } + + }; + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/BaseThread.java b/src/main/java/org/galemc/gale/executor/thread/BaseThread.java new file mode 100644 index 0000000000000000000000000000000000000000..8c7023025f6990eacf28186dbf42b275447b13f3 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/BaseThread.java @@ -0,0 +1,564 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.thread; + +import net.minecraft.server.MinecraftServer; +import org.galemc.gale.executor.annotation.Access; +import org.galemc.gale.executor.annotation.AnyThreadSafe; +import org.galemc.gale.executor.annotation.Guarded; +import org.galemc.gale.executor.annotation.PotentiallyBlocking; +import org.galemc.gale.executor.annotation.PotentiallyYielding; +import org.galemc.gale.executor.annotation.ThisBaseThreadOnly; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.lock.YieldingLock; +import org.galemc.gale.executor.queue.AbstractTaskQueue; +import org.galemc.gale.executor.queue.AllLevelsScheduledChunkCacheTaskQueue; +import org.galemc.gale.executor.queue.AllLevelsScheduledMainThreadChunkTaskQueue; +import org.galemc.gale.executor.queue.BaseTaskQueues; +import org.galemc.gale.executor.thread.wait.SignalReason; +import org.galemc.gale.executor.thread.wait.TaskWaitingBaseThreads; +import org.jetbrains.annotations.Nullable; +import org.spigotmc.WatchdogThread; +import io.papermc.paper.util.TickThread; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.BooleanSupplier; + +/** + * This class allows using instanceof to quickly check if the current thread is a base thread, i.e. + * the {@link MinecraftServer#originalServerThread} or a {@link SecondaryThread}, + * and contains information present on any base thread. + *
    + * Since the {@link WatchdogThread#instance} is also a {@link TickThread}, it is also an instance of this class. + * When active, it behaves as much as possible like the {@link MinecraftServer#originalServerThread} because only one of them + * may be usable at a time. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public abstract class BaseThread extends Thread { + + /** + * The maximum yield depth, beyond which a thread can no longer start more potentially yielding tasks. + */ + public static final int MAXIMUM_YIELD_DEPTH = Integer.getInteger("gale.max.yield.depth", 100); + + /** + * Wait for a limited time only before briefly waking up. This is the minimum time to wait for. + *
    + * This at least serves to wake up the thread to re-check the {@code stopCondition} in {@link #runTasksUntil}. + */ + private static final long MIN_WAIT_CONDITION_TIMEOUT_MICROS = 1L; + + /** + * Equivalent to {@link #MIN_WAIT_CONDITION_TIMEOUT_MICROS}, but this is the default time to wait for, + * unless a non-null {@code stopTime} is provided to {@link #await}. + */ + private static final long DEFAULT_WAIT_CONDITION_TIMEOUT_MICROS = 100L; + + public static final AbstractTaskQueue[] taskQueues = { + BaseTaskQueues.deferredToMainThread, + BaseTaskQueues.mainThreadTick, + BaseTaskQueues.anyTickScheduledMainThread, + BaseTaskQueues.baseThreadTick, + BaseTaskQueues.allLevelsScheduledChunkCache, + BaseTaskQueues.allLevelsScheduledMainThreadChunk, + BaseTaskQueues.scheduledAsync + }; + + public static final AbstractTaskQueue[] mainThreadOnlyTaskQueues; + public static final AbstractTaskQueue[] nonMainThreadOnlyTaskQueues; + static { + mainThreadOnlyTaskQueues = Arrays.stream(taskQueues).filter(AbstractTaskQueue::isMainThreadOnly).toArray(AbstractTaskQueue[]::new); + nonMainThreadOnlyTaskQueues = Arrays.stream(taskQueues).filter(queue -> !queue.isMainThreadOnly()).toArray(AbstractTaskQueue[]::new); + } + + public final int baseThreadIndex; + + /** + * The current yield depth of this thread. + */ + @ThisBaseThreadOnly + private int yieldDepth = 0; + + /** + * Whether this thread is currently restricted. + */ + @ThisBaseThreadOnly + public boolean isRestricted = false; + + /** + * The queue that the last polled task was successfully polled from. + */ + @ThisBaseThreadOnly + private AbstractTaskQueue lastPolledFromTaskQueue = null; + + /** + * Whether this thread is currently executing a main-thread-only task. + */ + @ThisBaseThreadOnly + public final AtomicInteger mainThreadOnlyTasksBeingExecutedCount = new AtomicInteger(); + + /** + * The lock to guard this thread's sleeping and waking actions. + */ + private final Lock waitLock = new ReentrantLock(); + + /** + * The condition to wait for a signal, when this thread has to wait for something to do. + */ + private final Condition waitCondition = waitLock.newCondition(); + + /** + * Whether this thread is currently not working on the content of a task, but instead + * attempting to poll a next task to do, checking whether it can accept tasks at all, or + * attempting to acquire a {@link YieldingLock}. + */ + @AnyThreadSafe(Access.READ) @ThisBaseThreadOnly(Access.WRITE) + protected volatile boolean isPollingTask = true; + + /** + * Whether this thread should not start waiting for something to do the next time no task could be polled, + * but instead try polling a task again. + */ + @AnyThreadSafe + private volatile boolean skipNextWait = false; + + /** + * Whether this thread is currently waiting for something to do. + */ + @AnyThreadSafe(Access.READ) @ThisBaseThreadOnly(Access.WRITE) + @Guarded("#waitLock") + private volatile boolean isWaiting = false; + + /** + * The last reason this thread was signalled before the current poll attempt, or null if the current + * poll attempt was not preceded by signalling (but by yielding for example). + */ + public volatile @Nullable SignalReason lastSignalReason = null; + + protected BaseThread(Runnable target, String name, int baseThreadIndex) { + super(target, name); + this.baseThreadIndex = baseThreadIndex; + } + + /** + * Yields to tasks, which means incrementing the yield depth, and polling and executing tasks while possible + * and the stop condition is not met, then decrementing the yield depth again. + * The stop condition is met if {@code stopCondition} is not null and returns true, or alternatively, + * if {@code stopCondition} is null, and {@code yieldingLock} is successfully acquired. + * When no tasks can be polled, this thread will wait for either a task that can be executed by this thread + * to become available, or for the {@code yieldingLock}, if given, to be released. + *
    + * Exactly one of {@code stopCondition} and {@code yieldingLock} must be non-null. + */ + @ThisBaseThreadOnly @PotentiallyYielding("this method is meant to yield") + public void yieldUntil(@Nullable BooleanSupplier stopCondition, @Nullable YieldingLock yieldingLock) { + this.yieldDepth++; + if (!this.isRestricted) { + if (this.yieldDepth >= MAXIMUM_YIELD_DEPTH || this.mainThreadOnlyTasksBeingExecutedCount.get() > 0) { + this.isRestricted = true; + SecondaryThreadPool.threadBecameRestricted(); + } + } + this.runTasksUntil(stopCondition, yieldingLock, null); + this.yieldDepth--; + if (this.isRestricted) { + if (this.yieldDepth < MAXIMUM_YIELD_DEPTH && (this.yieldDepth == 0 || this.mainThreadOnlyTasksBeingExecutedCount.get() == 0)) { + this.isRestricted = false; + SecondaryThreadPool.threadIsNoLongerRestricted(); + } + } + } + + /** + * This method will keep attempting to find a task to do, and execute it, and if none is found, registering itself + * with the places where a relevant task may be added in order to be signalled when one is actually added. + * The loop is broken as soon as the stop condition becomes true, or the given lock is successfully acquired, + * or {@link System#nanoTime} reaches the given {@code stopTime}. + *
    + * At least one of {@code stopCondition}, {@code yieldingLock} and {@code stopTime} must be non-null. + * Only one the above parameters may be non-null, except for the pair of {@code stopCondition} and {@code stopTime}: + * in which case running tasks continues until the stop condition becomes true, but {@link #await} is called + * with the given {@code stopTime}. + * + * @see #yieldUntil + */ + @ThisBaseThreadOnly @PotentiallyYielding("may yield further if an executed task is potentially yielding") + public void runTasksUntil(@Nullable BooleanSupplier stopCondition, @Nullable YieldingLock yieldingLock, @Nullable Long stopTime) { + /* + Endless loop that attempts to perform a task, and if one is found, tries to perform another again, + but if none is found, starts awaiting such a task to become available, or for the given yielding lock + to be released. + */ + /* + Make sure we do not allow the main thread to be claimed in between the moment we finish + executing a main-thread-only task, and we can poll the next one: we must as well stay the main thread if + we immediately start another main-thread-only task. + */ + boolean stillHaveToSetMainThreadCanBeClaimed = false; + this.skipNextWait = false; + // Reset the last signal reason before the next poll + this.lastSignalReason = null; + this.isPollingTask = true; + while (stopCondition != null ? !stopCondition.getAsBoolean() : yieldingLock != null ? !yieldingLock.tryLock() : System.nanoTime() < stopTime) { + MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking = MinecraftServer.isInSpareTime && MinecraftServer.blockingCount == 0 && !MinecraftServer.SERVER.haveTime(); + if (this.canAcceptTasks()) { + // Get a task that we can execute + Runnable task = this.pollTask(); + if (task != null) { + boolean isTaskMainThreadOnly = this.lastPolledFromTaskQueue.isMainThreadOnly(); + if (isTaskMainThreadOnly) { + stillHaveToSetMainThreadCanBeClaimed = true; + /* + We successfully polled a main-thread-only task, if there are more left in the queue, + make sure we skip the {@link MinecraftServer#mayHaveDelayedTasks()} check next time. + */ + if (!MinecraftServer.nextTimeAssumeWeMayHaveDelayedTasks && this.lastPolledFromTaskQueue.hasTasks()) { + MinecraftServer.nextTimeAssumeWeMayHaveDelayedTasks = true; + } + } else { + /* + If we still have to allow other threads to become the main thread, + do so before starting a task that is not main-thread-only. + */ + if (stillHaveToSetMainThreadCanBeClaimed) { + stillHaveToSetMainThreadCanBeClaimed = false; + MainThreadClaim.setMainThreadCanBeClaimed(); + } + } + this.isPollingTask = false; + if (isTaskMainThreadOnly) { + this.mainThreadOnlyTasksBeingExecutedCount.incrementAndGet(); + } + task.run(); + if (isTaskMainThreadOnly) { + MinecraftServer.SERVER.executeMidTickTasks(); // Paper - execute chunk tasks mid tick + } + if (isTaskMainThreadOnly) { + this.mainThreadOnlyTasksBeingExecutedCount.decrementAndGet(); + } + this.isPollingTask = true; + // Reset the last signal reason before the next poll + this.lastSignalReason = null; + continue; + } + } + // If we still have to allow other threads to become the main thread, do so before waiting + if (stillHaveToSetMainThreadCanBeClaimed) { + stillHaveToSetMainThreadCanBeClaimed = false; + if (this.mainThreadOnlyTasksBeingExecutedCount.get() == 0) { + MainThreadClaim.setMainThreadCanBeClaimed(); + } + } + /* + If no task that can be executed was found, wait for one to become available, + or for the given yielding lock to be released. This is the only time we should ever block inside + a potentially yielding procedure. + */ + this.await(yieldingLock, stopTime); + } + if (this.lastSignalReason != null && yieldingLock != null && this.lastSignalReason != yieldingLock.getSignalReason()) { + // The thread was signalled for another reason than the lock - but we acquired the lock instead + this.lastSignalReason.signalAnother(); + } + this.isPollingTask = false; + /* + If we still have to allow other threads to become the main thread, do so before exiting running tasks. + */ + if (stillHaveToSetMainThreadCanBeClaimed) { + if (this.mainThreadOnlyTasksBeingExecutedCount.get() == 0) { + MainThreadClaim.setMainThreadCanBeClaimed(); + } + } + } + + /** + * @return Whether this thread can currently accept more tasks. This is true if either the task is not restricted + * (i.e. it has not exceeded the maximum yield depth), or if the task is restricted but there are no pending + * potentially yielding tasks that a replacement thread could start if this thread were to idle. + */ + @ThisBaseThreadOnly @YieldFree + protected boolean canAcceptTasks() { + /* + This thread cannot accept new tasks if it is a surplus thread in the thread pool, + and not enough threads are restricted to warrant use of this surplus thread. + */ + if (this.baseThreadIndex >= SecondaryThreadPool.intendedSecondaryParallelism + 1 + SecondaryThreadPool.restrictedBaseThreadCount.get()) { + return false; + } + /* + Otherwise, this thread can always accept new tasks if it is not restricted, i.e. if it has not exceeded the + maximum yield depth and if it is not currently yielding from a main-thread-only task. + */ + if (!this.isRestricted) { + return true; + } + /* + Lastly, check if there are any yielding tasks that could be performed by other non-restricted: + if there are, since we are restricted, we should let other threads start executing those tasks instead. + */ + for (var queue : nonMainThreadOnlyTaskQueues) { + if (queue.hasYieldingTasksThatCanStartNow()) { + return false; + } + } + if (MainThreadClaim.canMainThreadBeClaimed) { + for (var queue : mainThreadOnlyTaskQueues) { + if (queue.hasYieldingTasksThatCanStartNow()) { + return false; + } + } + } + return true; + } + +// public boolean doesAnyMainThreadOnlyQueueHaveAppropriateTasks() { +// for (var queue : mainThreadOnlyTaskQueues) { +// if (queue.hasTasksThatCanStartNow(this)) { +// return true; +// } +// } +// return false; +// } + + /** + * Polls a task from any queue this thread can currently poll from, and returns it. + * Polling main-thread-only tasks is attempted before polling tasks that can be performed on any thread, + * and secondarily, polling potentially yielding tasks is attempted before yield-free tasks. + * + * @return The task that was polled, or null if no task was found. + */ + @ThisBaseThreadOnly @YieldFree + protected @Nullable Runnable pollTask() { + boolean attemptedToClaimMeanThread = false; + boolean hasJustClaimedMainThread = false; + for (var queue : taskQueues) { + // Claim the main thread if needed for this queue + boolean isQueueMainThreadOnly = queue.isMainThreadOnly(); + if (isQueueMainThreadOnly) { + if (!attemptedToClaimMeanThread) { + // Don't attempt to claim the main thread if this queue is empty anyway + if (!queue.hasTasksThatCanStartNow(this)) { + continue; + } + if (MainThreadClaim.attemptToClaim(this, true)) { + hasJustClaimedMainThread = true; + } + attemptedToClaimMeanThread = true; + } + // Skip this queue if we have failed to claim the main thread + if (!hasJustClaimedMainThread) { + continue; + } + } + Runnable task = queue.poll(this); + if (task != null) { + // We successfully polled a task + if (hasJustClaimedMainThread && !isQueueMainThreadOnly) { + /* + We claimed the main thread but polled from a queue that is not main-thread-only, so we must + mark the main thread as claimable again. + */ + MainThreadClaim.setMainThreadCanBeClaimed(); + } + this.lastPolledFromTaskQueue = queue; + return task; + } + } + if (hasJustClaimedMainThread) { + /* + We lost a race condition between the hasTasksThatCanStartNow checks and the poll call to a main-thread-only + task queue: let's just mark that the main thread can be claimed by another thread, and return that we + failed to poll a task. + */ + MainThreadClaim.setMainThreadCanBeClaimed(); + } + // We failed to poll any task + return null; + } + + /** + * Performs a dry run of {@link #pollTask}, returning whether a task would have been polled. + * + * @see #pollTask + */ + @ThisBaseThreadOnly @YieldFree + protected boolean couldPollTask() { + boolean checkedIfCouldClaimMeanThread = false; + boolean couldClaimMainThread = false; + for (var queue : taskQueues) { + // Check if this thread could claim the main thread if needed for this queue + boolean alreadyVerifiedHasTasksThatCanStartNow = false; + if (queue.isMainThreadOnly()) { + if (!checkedIfCouldClaimMeanThread) { + // Don't attempt to check to claim the main thread if this queue is empty anyway + if (!queue.hasTasksThatCanStartNow(this)) { + continue; + } + alreadyVerifiedHasTasksThatCanStartNow = true; + if (MainThreadClaim.couldClaim(this)) { + couldClaimMainThread = true; + } + checkedIfCouldClaimMeanThread = true; + } + // Skip this queue if we could not claim the main thread + if (!couldClaimMainThread) { + continue; + } + } + if (alreadyVerifiedHasTasksThatCanStartNow || queue.hasTasksThatCanStartNow(this)) { + return true; + } + } + // We failed to peek any task + return false; + } + + /** + * Signals this thread to wake up, or if it was not sleeping but attempting to poll a task: + * to not go to sleep the next time no task could be polled, and instead try polling a task again. + * + * @param reason The reason why this thread was signalled, or null if it is irrelevant (e.g. when the signal + * will never need to be repeated because there is only thread waiting for this specific event + * to happen). + * @return Whether this thread was sleeping before, and has woken up now. + */ + @AnyThreadSafe @YieldFree + public boolean signal(@Nullable SignalReason reason) { + //noinspection StatementWithEmptyBody + while (!this.waitLock.tryLock()); + try { + if (this.isWaiting) { + this.lastSignalReason = reason; + this.waitCondition.signal(); + return true; + } else if (this.isPollingTask) { + this.lastSignalReason = reason; + this.skipNextWait = true; + return true; + } + return false; + } finally { + this.waitLock.unlock(); + } + } + + /** + * Starts waiting on something to do. + * + * @param yieldingLock A {@link YieldingLock} to register with, or null if this thread is not waiting for + * a yielding lock. + * @param stopTime The maximum moment to wait until, or null if no specific time is known, in which case this + * thread will await being signalled without a timeout. + */ + @ThisBaseThreadOnly @PotentiallyBlocking + private void await(@Nullable YieldingLock yieldingLock, @Nullable Long stopTime) { + // Register this thread with parties that may signal it + boolean registeredAsWaiting = false; + if (!this.skipNextWait) { + // No point in registering if we're not going to wait anyway + this.registerAsWaiting(yieldingLock); + registeredAsWaiting = true; + } + // If we cannot acquire the lock, this thread is being signalled, so there is no reason to start waiting. + if (this.waitLock.tryLock()) { + try { + if (!this.skipNextWait) { + if (!registeredAsWaiting) { + this.registerAsWaiting(yieldingLock); + registeredAsWaiting = true; + } + // Do a quick last check to not sleep if something changed since last checking + if (!this.couldPollTask()) { + // Wait + this.isWaiting = true; + try { + if (stopTime == null) { + this.waitCondition.await(); + } else { + this.waitCondition.await(Math.max(MIN_WAIT_CONDITION_TIMEOUT_MICROS, Math.max((stopTime - System.nanoTime()) / 1000L, DEFAULT_WAIT_CONDITION_TIMEOUT_MICROS)), TimeUnit.MICROSECONDS); + } + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + this.isWaiting = false; + } + } + this.skipNextWait = false; + } finally { + this.waitLock.unlock(); + } + } + // Unregister this thread from the parties it was registered with before + if (registeredAsWaiting) { + this.unregisterAsWaiting(yieldingLock); + } + } + + /** + * Registers this thread in various places that will help to identify this thread as a candidate to signal + * in case of changes that allow this thread to continue work. + * + * @param yieldingLock A {@link YieldingLock} to register with, or null if this thread is not waiting for + * a yielding lock. + */ + @ThisBaseThreadOnly @YieldFree + private void registerAsWaiting(@Nullable YieldingLock yieldingLock) { + // Register with the queues that we may poll tasks from + TaskWaitingBaseThreads.add(this); + // Register with the lock + if (yieldingLock != null) { + yieldingLock.addWaitingThread(this); + } + } + + /** + * Unregisters this thread from the places it was registered with in {@link #registerAsWaiting}. + */ + @ThisBaseThreadOnly @YieldFree + private void unregisterAsWaiting(@Nullable YieldingLock yieldingLock) { + // Unregister from the task queues + TaskWaitingBaseThreads.remove(this); + // Unregister from the lock + if (yieldingLock != null) { + yieldingLock.removeWaitingThread(this); + } + } + + /** + * A thread-local iteration index for iterating over the levels in + * {@link AllLevelsScheduledChunkCacheTaskQueue#poll}. + */ + public int allLevelsChunkCacheTaskQueueLevelIterationIndex; + + /** + * A thread-local iteration index for iterating over the levels in + * {@link AllLevelsScheduledMainThreadChunkTaskQueue#poll}. + */ + public int allLevelsScheduledMainThreadChunkTaskQueueLevelIterationIndex; + + /** + * @return The current thread if it is a {@link BaseThread}, or null otherwise. + */ + @AnyThreadSafe @YieldFree + public static @Nullable BaseThread getCurrent() { + if (Thread.currentThread() instanceof BaseThread baseThread) { + return baseThread; + } + return null; + } + + /** + * @return Whether the current thread is a {@link BaseThread}. + */ + @AnyThreadSafe @YieldFree + public static boolean isBaseThread() { + return Thread.currentThread() instanceof BaseThread; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/MainThreadClaim.java b/src/main/java/org/galemc/gale/executor/thread/MainThreadClaim.java new file mode 100644 index 0000000000000000000000000000000000000000..d424c53c3d627d1e5265081ff99dbca6b91f15f4 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/MainThreadClaim.java @@ -0,0 +1,250 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.thread; + +import net.minecraft.server.MinecraftServer; +import org.galemc.gale.concurrent.Mutex; +import org.galemc.gale.executor.MinecraftServerBlockableEventLoop; +import org.galemc.gale.executor.annotation.Access; +import org.galemc.gale.executor.annotation.AnyThreadSafe; +import org.galemc.gale.executor.annotation.BaseThreadOnly; +import org.galemc.gale.executor.annotation.Guarded; +import org.galemc.gale.executor.annotation.MainThreadOnly; +import org.galemc.gale.executor.annotation.OriginalServerThreadOnly; +import org.galemc.gale.executor.annotation.PotentiallyYielding; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.thread.wait.SignalReason; +import org.galemc.gale.executor.thread.wait.TaskWaitingBaseThreads; +import org.jetbrains.annotations.Nullable; + +import java.util.function.BooleanSupplier; + +/** + * This class serves to remove the notion of 'main thread' from any specific thread, and instead make it + * a specific designation that any thread can claim. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public final class MainThreadClaim { + + private MainThreadClaim() {} + + /** + * Whether the main thread can currently be claimed. This is set to false after a thread becomes the main thread + * and is still attempting to poll a main thread task, or when a main-thread-only task is active (including + * the code surrounding any task execution loop calls made by the {@link MinecraftServer#originalServerThread}). + */ + @AnyThreadSafe(Access.READ) @MainThreadOnly(Access.WRITE) + @Guarded(value = "#lock", fieldAccess = Access.WRITE, except = "when only speculatively read") + public static volatile boolean canMainThreadBeClaimed = false; + + /** + * Whether the {@link MinecraftServer#originalServerThread} is wanting to become the main thread so that it + * can exit a part of code during which secondary threads can become the main thread. + * To be precise, the main thread can be claimed by secondary threads during ticking the server (because + * the {@link MinecraftServer#originalServerThread} then starts doing the same task-execution loop as secondary + * threads, with the exception that it will break this loop when ticking finishes - and to break the loop it + * must be the main thread) and during a {@link MinecraftServerBlockableEventLoop#managedBlock(BooleanSupplier)} + * during spare time that follows ticking (because during that time, other threads should be able to become the main + * thread to make sure main-thread-only tasks get executed speedily when they become available while the + * {@link MinecraftServer#originalServerThread} is working on non-main-thread-only tasks). + *
    + * This value is true until the {@link MinecraftServer#originalServerThread} starts allowing secondary threads + * to claim the main thread again. + */ + @AnyThreadSafe(Access.READ) @OriginalServerThreadOnly(Access.WRITE) + @Guarded(value = "#lock", fieldAccess = Access.WRITE) + private static volatile boolean isOriginalServerThreadWantingToClaim = true; + + /** + * The current main thread. Will be first initialized to {@link MinecraftServer#originalServerThread}. + */ + @AnyThreadSafe(Access.READ) + @Guarded(value = "#lock", fieldAccess = Access.WRITE, except = "in initialize(), where acquiring the lock is unnecessary") + private static volatile Thread mainThread; + + /** + * A lock for {@link #canMainThreadBeClaimed} and {@link #mainThread}, which are jointly + * checked or updated. + */ + private static final Mutex lock = Mutex.create(); + + /** + * Sets the current main thread to {@link MinecraftServer#originalServerThread}. + */ + @OriginalServerThreadOnly + public static void initialize() { + mainThread = MinecraftServer.SERVER.originalServerThread;; + } + + /** + * @deprecated Use {@link #attemptToClaim(Thread, boolean)} instead. + */ + @BaseThreadOnly @YieldFree + @Deprecated + public static boolean attemptToClaim(boolean setCanMainThreadBeClaimedToFalse) { + return attemptToClaim(Thread.currentThread(), setCanMainThreadBeClaimedToFalse); + } + + /** + * Makes the current thread the main thread, if possible. + * This is possible if and only if {@link #canMainThreadBeClaimed} is true or the current thread + * is already the main thread. + * If successful, and {@code setCanMainThreadBeClaimedToFalse} is true, + * sets {@link #canMainThreadBeClaimed} to false. + * + * @return Whether the current thread is now the main thread. + */ + @BaseThreadOnly @YieldFree + public static boolean attemptToClaim(Thread currentThread, boolean setCanMainThreadBeClaimedToFalse) { + // Fast pre-check to avoid acquiring the lock + if (!canMainThreadBeClaimed || (isOriginalServerThreadWantingToClaim && currentThread != MinecraftServer.SERVER.originalServerThread)) { + /* + If true, we are already the main thread and no-one can have claimed it by this point. + If false cannot become the main thread right now, maybe it just became possible during the last + line of code, but it's not worth acquiring the lock for. + */ + if (mainThread == currentThread) { + if (!setCanMainThreadBeClaimedToFalse || !canMainThreadBeClaimed) { + // We can only return if we do not still need to set canMainThreadBeClaimed + return true; + } + } else { + return false; + } + } + // Acquire the lock and make an attempt + //noinspection StatementWithEmptyBody + while (!lock.tryAcquire()); + try { + if (!canMainThreadBeClaimed || (isOriginalServerThreadWantingToClaim && currentThread != MinecraftServer.SERVER.originalServerThread)) { + if (mainThread == currentThread) { + if (setCanMainThreadBeClaimedToFalse) { + canMainThreadBeClaimed = false; + } + return true; + } + return false; + } + mainThread = currentThread; + if (setCanMainThreadBeClaimedToFalse) { + canMainThreadBeClaimed = false; + } + return true; + } finally { + lock.release(); + } + } + + /** + * @deprecated Use {@link #couldClaim(Thread)} instead. + */ + @BaseThreadOnly @YieldFree + @Deprecated + public static boolean couldClaim() { + return couldClaim(Thread.currentThread()); + } + + /** + * Performs a dry run of {@link #attemptToClaim(Thread, boolean)}, returning the same result but not actually + * claiming the main thread. + * + * @see #attemptToClaim(Thread, boolean) + */ + @BaseThreadOnly @YieldFree + public static boolean couldClaim(Thread currentThread) { + // Fast pre-check to avoid acquiring the lock + if (!canMainThreadBeClaimed || (isOriginalServerThreadWantingToClaim && currentThread != MinecraftServer.SERVER.originalServerThread)) { + /* + If true, we are already the main thread and no-one can have claimed it by this point. + If false cannot become the main thread right now, maybe it just became possible during the last + line of code, but it's not worth acquiring the lock for. + */ + return mainThread == currentThread; + } + return true; + } + + /** + * Sets {@link #canMainThreadBeClaimed} to true: must only be called from the current main thread. + */ + @MainThreadOnly @YieldFree + public static void setMainThreadCanBeClaimed() { + //noinspection StatementWithEmptyBody + while (!lock.tryAcquire()); + try { + canMainThreadBeClaimed = true; + } finally { + lock.release(); + } + signalWaitingThreads(); + } + + /** + * @deprecated Use {@link #isCurrentThreadMainThreadAndNotClaimable(Thread)} instead. + */ + @AnyThreadSafe @YieldFree + @Deprecated + public static boolean isCurrentThreadMainThreadAndNotClaimable() { + return isCurrentThreadMainThreadAndNotClaimable(Thread.currentThread()); + } + + @AnyThreadSafe @YieldFree + public static boolean isCurrentThreadMainThreadAndNotClaimable(Thread currentThread) { + return !canMainThreadBeClaimed && currentThread == mainThread; + } + + /** + * Called by the {@link MinecraftServer#originalServerThread} to indicate that it requires being the main thread, + * and secondary threads can no longer claim the main thread from now on. + *
    + * This method waits until the main thread can be claimed for the current thread, yielding to pending tasks + * until then. + */ + @OriginalServerThreadOnly @PotentiallyYielding + public static void claimAsOriginalServerThread() { + //noinspection StatementWithEmptyBody + while (!lock.tryAcquire()); + try { + isOriginalServerThreadWantingToClaim = true; + } finally { + lock.release(); + } + MinecraftServer.SERVER.originalServerThread.yieldUntil(() -> attemptToClaim(MinecraftServer.SERVER.originalServerThread, true), null); + } + + /** + * Called by the {@link MinecraftServer#originalServerThread} to indicate that secondary threads can + * claim the main thread from now on. + */ + @OriginalServerThreadOnly @MainThreadOnly @YieldFree + public static void secondaryThreadsCanStartToClaim() { + //noinspection StatementWithEmptyBody + while (!lock.tryAcquire()); + try { + isOriginalServerThreadWantingToClaim = false; + canMainThreadBeClaimed = true; + } finally { + lock.release(); + } + signalWaitingThreads(); + } + + private static void signalWaitingThreads() { + for (var queue : BaseThread.mainThreadOnlyTaskQueues) { + if (queue.hasYieldingTasksThatCanStartNow()) { + @Nullable SignalReason yieldingSignalReason = queue.getYieldingSignalReason(); + if (yieldingSignalReason != null && yieldingSignalReason.signalAnother()) { + return; + } + } + if (queue.hasFreeTasksThatCanStartNow()) { + @Nullable SignalReason freeSignalReason = queue.getFreeSignalReason(); + if (freeSignalReason != null && freeSignalReason.signalAnother()) { + return; + } + } + } + } + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/MainThreadDeferral.java b/src/main/java/org/galemc/gale/executor/thread/MainThreadDeferral.java new file mode 100644 index 0000000000000000000000000000000000000000..017a5ab8c2468cc59142be7c28a78622e6646dde --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/MainThreadDeferral.java @@ -0,0 +1,132 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.thread; + +import org.galemc.gale.concurrent.UnterminableExecutorService; +import org.galemc.gale.executor.queue.BaseTaskQueues; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.function.Supplier; + +/** + * This class provides functionality to allow any thread, including a {@link BaseThread}, to defer blocks of code to the + * main thread, and wait for their completion. In other words, instead of the typical paradigm where a block of code + * is executed while a lock is held by the thread, we do not acquire a lock, but instead schedule the code to be run on + * the main thread and wait for that to happen. + *
    + * This has a number of advantages. Code that checks whether it is being run on the main thread can be run this + * way. Since these parts of code are always performed on the main thread regardless of the thread requesting them + * to be run, there is no chance of deadlock occurring from two different locks being desired in a different order + * on two threads. Also, we can yield from a {@link BaseThread} until the code has been executed. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public final class MainThreadDeferral { + + private MainThreadDeferral() {} + + /** + * @see #defer(Supplier, boolean) + */ + public static void defer(Runnable task, boolean yielding) { + defer(task, null, yielding); + } + + /** + * Runs the given {@code task} on the main thread, and yields until it has finished. + * If this thread is already the main thread, the task will be executed right away. + *
    + * Like any potentially yielding method, while technically possible to call from any thread, this method should + * generally only be called from a {@link BaseThread}, because on any other thread, the thread will block until + * the given task has been completed by the main thread. + * + * @param task The task to run on the main thread. + * @param yielding Whether the given {@code task} is potentially yielding. + */ + public static T defer(Supplier task, boolean yielding) { + return defer(null, task, yielding); + } + + /** + * Common implementation for {@link #defer(Runnable, boolean)} and {@link #defer(Supplier, boolean)}. + * Exactly one of {@code runnable} or {@code supplier} must be non-null. + */ + private static T defer(@Nullable Runnable runnable, @Nullable Supplier supplier, boolean yielding) { + // Become the main thread if possible + BaseThread baseThread = BaseThread.getCurrent(); + if (baseThread != null && MainThreadClaim.attemptToClaim(baseThread, true)) { + // Run the task right away + baseThread.mainThreadOnlyTasksBeingExecutedCount.incrementAndGet(); + T output; + if (supplier == null) { + runnable.run(); + output = null; + } else { + output = supplier.get(); + } + if (baseThread.mainThreadOnlyTasksBeingExecutedCount.decrementAndGet() == 0) { + // Make the main thread claimable again + MainThreadClaim.setMainThreadCanBeClaimed(); + } + return output; + } + // Otherwise, schedule the task and wait for it to complete + CompletableFuture future = new CompletableFuture<>(); + if (baseThread != null) { + // Yield until the task completes + BaseTaskQueues.deferredToMainThread.add(() -> { + if (supplier == null) { + runnable.run(); + future.complete(null); + } else { + future.complete(supplier.get()); + } + baseThread.signal(null); + }, yielding); + baseThread.yieldUntil(future::isDone, null); + return future.getNow(null); + } else { + // Block until the task completes + BaseTaskQueues.deferredToMainThread.add(() -> { + if (supplier == null) { + runnable.run(); + future.complete(null); + } else { + future.complete(supplier.get()); + } + }, yielding); + return future.join(); + } + } + + /** + * An executor for deferring potentially yielding, tasks to the main thread, + * where {@link Executor#execute} calls {@link #defer}. + */ + public static final ExecutorService yieldingExecutor = new UnterminableExecutorService() { + + @Override + public void execute(@NotNull Runnable runnable) { + defer(runnable, true); + } + + }; + + /** + * An executor for deferring yield-free, tasks to the main thread, + * where {@link Executor#execute} calls {@link #defer}. + */ + public static final ExecutorService freeExecutor = new UnterminableExecutorService() { + + @Override + public void execute(@NotNull Runnable runnable) { + defer(runnable, false); + } + + }; + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/SecondaryThread.java b/src/main/java/org/galemc/gale/executor/thread/SecondaryThread.java new file mode 100644 index 0000000000000000000000000000000000000000..14ce3eb68983b7d53062e16bfa0da352562364a6 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/SecondaryThread.java @@ -0,0 +1,42 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.thread; + +import net.minecraft.server.MinecraftServer; +import org.galemc.gale.executor.annotation.BaseThreadOnly; +import org.galemc.gale.executor.annotation.ThisBaseThreadOnly; + +/** + * A secondary thread to perform tasks in parallel to the {@link MinecraftServer#originalServerThread}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public class SecondaryThread extends BaseThread { + + public final int secondaryThreadIndex; + + public SecondaryThread(int secondaryThreadIndex) { + super(SecondaryThread::getInstanceAndRunForever, "Secondary Thread " + secondaryThreadIndex, secondaryThreadIndex + 1); + this.secondaryThreadIndex = secondaryThreadIndex; + } + + /** + * This is the main thread loop for a {@link SecondaryThread}. + * It will loop forever, always attempting to find a task to do, and if none is found, registering itself + * with the places where a relevant task may be added in order to be signalled when one is actually added. + */ + @ThisBaseThreadOnly + private void runForever() { + this.runTasksUntil(() -> false, null, null); + } + + /** + * A method that simply acquires the {@link SecondaryThread} that is the current thread, and calls + * {@link #runForever()} on it. + */ + @BaseThreadOnly + private static void getInstanceAndRunForever() { + ((SecondaryThread) Thread.currentThread()).runForever(); + } + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/SecondaryThreadPool.java b/src/main/java/org/galemc/gale/executor/thread/SecondaryThreadPool.java new file mode 100644 index 0000000000000000000000000000000000000000..312c57bd7d4ea627e39b797c782be3f893564b22 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/SecondaryThreadPool.java @@ -0,0 +1,225 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.thread; + +import net.minecraft.server.MinecraftServer; +import org.galemc.gale.concurrent.Mutex; +import org.galemc.gale.executor.annotation.Access; +import org.galemc.gale.executor.annotation.AnyThreadSafe; +import org.galemc.gale.executor.annotation.BaseThreadOnly; +import org.galemc.gale.executor.annotation.Guarded; +import org.galemc.gale.executor.annotation.OriginalServerThreadOnly; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.thread.wait.SignalReason; +import org.galemc.gale.util.CPUCoresEstimation; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A pool of threads that, in addition to the main thread, all non-blocking tasks can be performed on. + * This pool manages the amount of threads necessary, and activates and de-activates surplus threads to avoid + * context switches. Threads created for this pool are instances of {@link SecondaryThread}. + *
    + * This pool intends to keep {@link #intendedSecondaryParallelism} + 1 threads active at any time, which is initially + * the {@link MinecraftServer#originalServerThread} and {@link #intendedSecondaryParallelism} secondary threads. + * It allows more secondary threads to become active if an existing active thread is unable to accept more tasks + * because it has become restricted. A {@link BaseThread} is restricted when it can not accept new tasks. This happens + * when it can only accept yield-free tasks, but there are still pending potentially yielding tasks, which means we + * prefer other threads to become active and start those potentially yielding tasks. A thread can only accept yield-free + * tasks when it has reached its maximum yield depth or is attempting to yield from a main-thread-only task. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public final class SecondaryThreadPool { + + private SecondaryThreadPool() {} + + /** + * Whether {@link SecondaryThread}s can poll only tasks that can run on any thread. + *
    + * This is true when {@link #intendedSecondaryParallelism} is first determined to have to be 0. In that case, + * it is set to 1 instead, so that we at least have one secondary thread to perform asynchronous tasks that + * may take so long they prevent a tick from finishing. + */ + public static final boolean canOnlyPollAsyncTasksOnSecondaryThreads; + + /** + * The minimum number of threads that will be actively in use by this pool. + *
    + * By default, we always do not use one core, so that there is always a core available for the main thread, or + * to run other important threads such as Netty, I/O or garbage collection on. + * This also means that if the system has 1 core only, we do not use any secondary threads by default. + *
    + * This value is never negative. + *
    + * The value is currently automatically determined according to the following table: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    system corescores spared
    ≤ 31
    [4, 14]2
    [15, 23]3
    [24, 37]4
    [38, 54]5
    [55, 74]6
    [75, 99]7
    [100, 127]8
    [128, 158]9
    [159, 193]10
    [194, 232]11
    [233, 274]12
    ≥ 27513
    + * Then minimum number of secondary threads = system cores - cores spared. + */ + public static final int intendedSecondaryParallelism; + static { + int parallelismByEnvironmentVariable = Integer.getInteger("gale.threads.secondary", -1); + int intendedSecondaryParallelismBeforeSetAtLeastOne; + if (parallelismByEnvironmentVariable >= 0) { + intendedSecondaryParallelismBeforeSetAtLeastOne = parallelismByEnvironmentVariable; + } else { + int systemCores = CPUCoresEstimation.get(); + int coresSpared; + if (systemCores <= 3) { + coresSpared = 1; + } else if (systemCores <= 14) { + coresSpared = 2; + } else if (systemCores <= 23) { + coresSpared = 3; + } else if (systemCores <= 37) { + coresSpared = 4; + } else if (systemCores <= 54) { + coresSpared = 5; + } else if (systemCores <= 74) { + coresSpared = 6; + } else if (systemCores <= 99) { + coresSpared = 7; + } else if (systemCores <= 127) { + coresSpared = 8; + } else if (systemCores <= 158) { + coresSpared = 9; + } else if (systemCores <= 193) { + coresSpared = 10; + } else if (systemCores <= 232) { + coresSpared = 11; + } else if (systemCores <= 274) { + coresSpared = 12; + } else { + coresSpared = 13; + } + intendedSecondaryParallelismBeforeSetAtLeastOne = systemCores - coresSpared; + } + if (intendedSecondaryParallelismBeforeSetAtLeastOne >= 1) { + intendedSecondaryParallelism = intendedSecondaryParallelismBeforeSetAtLeastOne; + canOnlyPollAsyncTasksOnSecondaryThreads = false; + } else { + intendedSecondaryParallelism = 1; + canOnlyPollAsyncTasksOnSecondaryThreads = true; + } + + } + + /** + * The base threads, which is an array of the {@link MinecraftServer#originalServerThread} at index 0, and + * {@link #secondaryThreads} afterwards. In other words, it is an array of every {@link BaseThread} among + * {@link MinecraftServer#originalServerThread} and {@link #secondaryThreads}, indexed by their + * {@link BaseThread#baseThreadIndex}. + */ + @AnyThreadSafe(Access.READ) @BaseThreadOnly(Access.WRITE) + @Guarded(value = "#threadsLock", fieldAccess = Access.WRITE, except = "during static initialization, where acquiring the lock is unnecessary") + private static BaseThread[] baseThreads = new BaseThread[intendedSecondaryParallelism + 1]; + + @OriginalServerThreadOnly @YieldFree + public static void setOriginalServerThreadInBaseThread() { + baseThreads[0] = MinecraftServer.SERVER.originalServerThread; + } + + @AnyThreadSafe @YieldFree + public static BaseThread getBaseThreadByIndex(int baseThreadIndex) { + return baseThreads[baseThreadIndex]; + } + + /** + * The secondary threads. It is an array of every {@link SecondaryThread}, indexed by their + * {@link SecondaryThread#secondaryThreadIndex}. + */ + @AnyThreadSafe(Access.READ) @BaseThreadOnly(Access.WRITE) + @Guarded(value = "#threadsLock", fieldAccess = Access.WRITE, except = "during static initialization, where acquiring the lock is unnecessary") + private static SecondaryThread[] secondaryThreads = new SecondaryThread[intendedSecondaryParallelism]; + + static { + for (int i = 0; i < secondaryThreads.length; i++) { + baseThreads[i + 1] = secondaryThreads[i] = new SecondaryThread(i); + } + } + + @AnyThreadSafe @YieldFree + public static SecondaryThread getSecondaryThreadByIndex(int secondaryThreadIndex) { + return secondaryThreads[secondaryThreadIndex]; + } + + private static final Mutex threadsLock = Mutex.create(); + + /** + * The number of base threads that are currently restricted. + */ + @AnyThreadSafe(Access.READ) @BaseThreadOnly(Access.WRITE) + public static final AtomicInteger restrictedBaseThreadCount = new AtomicInteger(); + + private static final SignalReason surplusThreadActivationSignalReason = SignalReason.createNonRetrying(); + + /** + * Starts all secondary threads in this pool. + */ + @OriginalServerThreadOnly @YieldFree + public static void startSecondaryThreads() { + //noinspection StatementWithEmptyBody + while (!threadsLock.tryAcquire()); + try { + for (SecondaryThread secondaryThread : secondaryThreads) { + secondaryThread.start(); + } + } finally { + threadsLock.release(); + } + } + + /** + * Called by a {@link BaseThread} when it becomes restricted. + */ + @BaseThreadOnly @YieldFree + public static void threadBecameRestricted() { + int newSecondaryThreadCount = restrictedBaseThreadCount.incrementAndGet() + intendedSecondaryParallelism; + if (newSecondaryThreadCount > secondaryThreads.length) { + //noinspection StatementWithEmptyBody + while (!threadsLock.tryAcquire()); + try { + // Check again to make sure secondaryThreads hasn't already been extended since the last check + int oldSecondaryThreadCount = secondaryThreads.length; + if (newSecondaryThreadCount > oldSecondaryThreadCount) { + // Add surplus threads + secondaryThreads = Arrays.copyOf(secondaryThreads, newSecondaryThreadCount); + baseThreads = Arrays.copyOf(baseThreads, newSecondaryThreadCount + 1); + for (int i = oldSecondaryThreadCount; i < newSecondaryThreadCount; i++) { + baseThreads[i + 1] = secondaryThreads[i] = new SecondaryThread(i); + secondaryThreads[i].start(); + } + } + } finally { + threadsLock.release(); + } + } + // Wake up the appropriate surplus thread if it was sleeping + secondaryThreads[newSecondaryThreadCount - 1].signal(surplusThreadActivationSignalReason); + } + + /** + * Called by a {@link BaseThread} when it becomes no longer restricted. + */ + @BaseThreadOnly @YieldFree + public static void threadIsNoLongerRestricted() { + restrictedBaseThreadCount.decrementAndGet(); + } + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/wait/SignalReason.java b/src/main/java/org/galemc/gale/executor/thread/wait/SignalReason.java new file mode 100644 index 0000000000000000000000000000000000000000..1c56597ccce2f11db1c8a903e24c07eeb2143564 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/wait/SignalReason.java @@ -0,0 +1,43 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.thread.wait; + +import org.galemc.gale.executor.annotation.AnyThreadSafe; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.thread.BaseThread; + +/** + * An interface to indicate the reason of a call to {@link BaseThread#signal}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public interface SignalReason { + + /** + * Signals another {@link BaseThread} that is waiting for the same reason. + * + * @return Whether any thread was signalled. + */ + boolean signalAnother(); + + @AnyThreadSafe @YieldFree + static SignalReason createForTaskQueue(boolean mainThreadOnly, boolean baseThreadOnly, boolean yielding) { + if (mainThreadOnly && !baseThreadOnly) { + throw new IllegalArgumentException(); + } + return new SignalReason() { + + @Override + public boolean signalAnother() { + return TaskWaitingBaseThreads.signal(this, mainThreadOnly, baseThreadOnly, yielding); + } + + }; + } + + @AnyThreadSafe @YieldFree + static SignalReason createNonRetrying() { + return () -> false; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/wait/TaskWaitingBaseThreads.java b/src/main/java/org/galemc/gale/executor/thread/wait/TaskWaitingBaseThreads.java new file mode 100644 index 0000000000000000000000000000000000000000..2623dea4cb52c16d43749538a5c0254e1ea92667 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/wait/TaskWaitingBaseThreads.java @@ -0,0 +1,124 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.thread.wait; + +import net.minecraft.server.MinecraftServer; +import org.galemc.gale.executor.annotation.AnyThreadSafe; +import org.galemc.gale.executor.annotation.BaseThreadOnly; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.MainThreadClaim; +import org.galemc.gale.executor.thread.SecondaryThreadPool; + +/** + * This class keeps track of all the {@link BaseThread}s that are waiting for tasks to be added to a task queue. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public final class TaskWaitingBaseThreads { + + private TaskWaitingBaseThreads() {} + + /** + * The base threads waiting for potentially yielding {@link BaseThread}-only tasks to be added. + */ + private static final WaitingBaseThreadSet forBaseThreadYieldingTasks = new WaitingBaseThreadSet(); + + /** + * The base threads waiting for yield-free {@link BaseThread}-only tasks to be added. + */ + private static final WaitingBaseThreadSet forBaseThreadFreeTasks = new WaitingBaseThreadSet() { + + @Override + public boolean pollAndSignal(SignalReason reason) { + if (super.pollAndSignal(reason)) { + return true; + } + /* + Waiting for BaseThread-only potentially yielding tasks implies waiting for BaseThread-only + yield-free tasks, so we signal thread waiting for potentially yielding tasks too. + */ + return forBaseThreadYieldingTasks.pollAndSignal(reason); + } + + }; + + /** + * The base threads waiting for potentially yielding any-thread tasks to be added. + */ + private static final WaitingBaseThreadSet forAnyThreadYieldingTasks = new WaitingBaseThreadSet(); + + /** + * The base threads waiting for yield-free any-thread tasks to be added. + */ + private static final WaitingBaseThreadSet forAnyThreadFreeTasks = new WaitingBaseThreadSet() { + + @Override + public boolean pollAndSignal(SignalReason reason) { + if (super.pollAndSignal(reason)) { + return true; + } + /* + Waiting for any-thread potentially yielding tasks implies waiting for any-thread + yield-free tasks, so we signal thread waiting for potentially yielding tasks too. + */ + return forAnyThreadYieldingTasks.pollAndSignal(reason); + } + + }; + + /** + * Signal a thread due to a new task being added. + * + * @param reason The {@link SignalReason} that the signal will originate from. + * @param mainThreadOnly Whether the added task is main-thread-only. + * @param baseThreadOnly Whether the added task is {@link BaseThread}-only. + * @param yielding Whether the added task is potentially yielding. + * @return Whether a thread was signalled (this return value is always accurate). + */ + @AnyThreadSafe @YieldFree + public static boolean signal(SignalReason reason, boolean mainThreadOnly, boolean baseThreadOnly, boolean yielding) { + if (mainThreadOnly && !MainThreadClaim.canMainThreadBeClaimed) { + return false; + } + return (baseThreadOnly ? (yielding ? forBaseThreadYieldingTasks : forBaseThreadFreeTasks) : (yielding ? forAnyThreadYieldingTasks : forAnyThreadFreeTasks)).pollAndSignal(reason); + } + + /** + * Adds a thread to the sets of threads waiting for new tasks to be added to task queues. + * + * @param currentThread The current thread, as well as the thread to register as waiting for new tasks. + */ + @BaseThreadOnly @YieldFree + public static void add(BaseThread currentThread) { + boolean isOriginalServerThread = MinecraftServer.SERVER == null || currentThread == MinecraftServer.SERVER.originalServerThread; + // If the thread can accept tasks that must run on a BaseThread, wait for those + if (isOriginalServerThread || !SecondaryThreadPool.canOnlyPollAsyncTasksOnSecondaryThreads) { + if (!currentThread.isRestricted) { + forBaseThreadYieldingTasks.add(currentThread); + } + forBaseThreadFreeTasks.add(currentThread); + } + // If the thread can accept tasks that can run on any thread, wait for those + if (!isOriginalServerThread || MinecraftServer.canPollAsyncTasksOnOriginalServerThread) { + if (!currentThread.isRestricted) { + forAnyThreadYieldingTasks.add(currentThread); + } + forAnyThreadFreeTasks.add(currentThread); + } + } + + /** + * Removes a thread from the sets of threads waiting for new tasks. + * + * @param currentThread The current thread, as well as the thread to unregister as waiting for new tasks. + */ + @BaseThreadOnly @YieldFree + public static void remove(BaseThread currentThread) { + forBaseThreadYieldingTasks.remove(currentThread); + forBaseThreadFreeTasks.remove(currentThread); + forAnyThreadYieldingTasks.remove(currentThread); + forAnyThreadFreeTasks.remove(currentThread); + } + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/wait/WaitingBaseThreadSet.java b/src/main/java/org/galemc/gale/executor/thread/wait/WaitingBaseThreadSet.java new file mode 100644 index 0000000000000000000000000000000000000000..b06c3a0e299e2907547510f88ee00742b9bd8150 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/wait/WaitingBaseThreadSet.java @@ -0,0 +1,75 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.thread.wait; + +import org.galemc.gale.executor.annotation.AnyThreadSafe; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.lock.YieldingLock; +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.SecondaryThreadPool; + +import java.util.concurrent.ConcurrentSkipListSet; + +/** + * A set of waiting {@link BaseThread}s. This is used to collect the threads that are all waiting for new work, + * e.g. for new tasks to be added to a queue or for a {@link YieldingLock} to be released. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@AnyThreadSafe @YieldFree +public class WaitingBaseThreadSet implements SignalReason { + + /** + * A set of the {@link BaseThread#baseThreadIndex} of the threads in this collection. + */ + private final ConcurrentSkipListSet skipListSet = new ConcurrentSkipListSet<>(); + + /** + * Adds a waiting {@link BaseThread}. + */ + public void add(BaseThread thread) { + this.skipListSet.add(thread.baseThreadIndex); + } + + /** + * Removes a waiting {@link BaseThread}. + */ + public void remove(BaseThread thread) { + this.skipListSet.add(thread.baseThreadIndex); + } + + public boolean pollAndSignal() { + return this.pollAndSignal(this); + } + + /** + * Attempts to signal one waiting {@link BaseThread}. + *
    + * Calling this method twice from different threads concurrently + * will never lead to signalling the same waiting thread: + * every thread is only signalled by this set at most one time per continuous period of time that it is present. + *
    + * Calling this method may fail to signal a newly added thread that is added after this method is called, + * but before it returns, and may also fail to signal a just removed thread + * that was removed after this method was called, but before it returned. + * + * @return Whether a thread was signalled (this return value is always accurate). + * + * @see BaseThread#signal + */ + @AnyThreadSafe @YieldFree + public boolean pollAndSignal(SignalReason reason) { + Integer baseThreadIndex = this.skipListSet.pollFirst(); + if (baseThreadIndex != null) { + SecondaryThreadPool.getBaseThreadByIndex(baseThreadIndex).signal(reason); + return true; + } + return false; + } + + @Override + public boolean signalAnother() { + return this.pollAndSignal(); + } + +} diff --git a/src/main/java/org/spigotmc/SpigotCommand.java b/src/main/java/org/spigotmc/SpigotCommand.java index 3112a8695639c402e9d18710acbc11cff5611e9c..7b38565b8699bd083c2114981feb2202321b8486 100644 --- a/src/main/java/org/spigotmc/SpigotCommand.java +++ b/src/main/java/org/spigotmc/SpigotCommand.java @@ -31,7 +31,7 @@ public class SpigotCommand extends Command { MinecraftServer console = MinecraftServer.getServer(); org.spigotmc.SpigotConfig.init((File) console.options.valueOf("spigot-settings")); - for (ServerLevel world : console.getAllLevels()) { + for (ServerLevel world : console.getAllLevelsArray()) { // Gale - base thread pool - optimize server levels world.spigotConfig.init(); } console.server.reloadCount++; diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java index 832f1ee4fb11c981bd109510eb908d7c7ef91bd4..970908ba3eafbc79f01c0681e214dfdb95658ec6 100644 --- a/src/main/java/org/spigotmc/WatchdogThread.java +++ b/src/main/java/org/spigotmc/WatchdogThread.java @@ -13,7 +13,7 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa { public static final boolean DISABLE_WATCHDOG = Boolean.getBoolean("disable.watchdog"); // Paper - private static WatchdogThread instance; + public static WatchdogThread instance; // Gale - base thread pool - private -> public private long timeoutTime; private boolean restart; private final long earlyWarningEvery; // Paper - Timeout time for just printing a dump but not restarting @@ -206,7 +206,7 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Gale!):" ); // Paper // Gale - branding changes io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(isLongTimeout); // Paper // Paper - rewrite chunk system this.dumpTickingInfo(); // Paper - log detailed tick information - WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log ); + WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().originalServerThread.getId(), Integer.MAX_VALUE ), log ); // Gale - base thread pool log.log( Level.SEVERE, "------------------------------" ); // // Paper start - Only print full dump on long timeouts