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 pools 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..238e7aa6e8a9e9f26bc6dee8d7e49a853c3cc0e2 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.BaseTaskQueues; + import java.util.ArrayDeque; import java.util.concurrent.atomic.AtomicLong; public class PrioritisedThreadedTaskQueue implements PrioritisedExecutor { + private final boolean influenceMayHaveDelayedTasks; // Gale - base thread pools + 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 pools + public PrioritisedThreadedTaskQueue(boolean influenceMayHaveDelayedTasks) { + this.influenceMayHaveDelayedTasks = influenceMayHaveDelayedTasks; + } + // Gale end - base thread pools + @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 pools + if (this.influenceMayHaveDelayedTasks) { + MinecraftServer.nextTimeAssumeWeMayHaveDelayedTasks = true; + BaseTaskQueues.allLevelsScheduledTickThreadChunk.signalReason.signalAnother(); + } + // Gale end - base thread pools return this.totalScheduledTasks.getAndAdd(value); } @@ -158,6 +179,12 @@ public class PrioritisedThreadedTaskQueue implements PrioritisedExecutor { return this.totalCompletedTasks.getAndAdd(value); } + // Gale start - base thread pools + public final boolean hasScheduledUncompletedTasksVolatile() { + return this.totalScheduledTasks.get() > this.totalCompletedTasks.get(); + } + // Gale end - base thread pools + 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 4f3670b2bdb8b1b252e9f074a6af56a018a8c465..757229093ecce162c99e27c2f92ead2e1a1a2b10 100644 --- a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java +++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java @@ -22,6 +22,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.ScheduledServerThreadTaskQueues; import java.util.*; import java.util.concurrent.Executor; @@ -181,7 +182,7 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo if (!Bukkit.isPrimaryThread()) { // Plugins? - MinecraftServer.getServer().scheduleOnMain(() -> modifyBlocks(chunkPacket, chunkPacketInfo)); + ScheduledServerThreadTaskQueues.add(() -> modifyBlocks(chunkPacket, chunkPacketInfo), ScheduledServerThreadTaskQueues.ANTI_XRAY_MODIFY_BLOCKS_TASK_MAX_DELAY); // Gale - base thread pools 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..3686b0330b48119e08cbc1528cfd86c5ec7bd8e9 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 pools - 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..9b94defa9520ac5c17c9b8bf1cfb3b17ddac2d22 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 pools - 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 3088d5f008a8cb5a75f1e11bd80a2614a4c1b75d..0c771f024cec0b3fbb68e4eeeeb778187a89456f 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 pools - remove Paper async executor + public static final Executor cleanerExecutor = BaseTaskQueues.cleaner.executor; // Gale - base thread pools - 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..88abb082a994d5dbc3b51e770acfbd4f0a7af1f9 100644 --- a/src/main/java/io/papermc/paper/util/TickThread.java +++ b/src/main/java/io/papermc/paper/util/TickThread.java @@ -3,10 +3,14 @@ package io.papermc.paper.util; 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.queue.AbstractTaskQueue; +import org.galemc.gale.executor.queue.BaseTaskQueues; +import org.galemc.gale.executor.thread.BaseYieldingThread; +import org.galemc.gale.executor.thread.pooled.ServerThreadPool; + import java.util.concurrent.atomic.AtomicInteger; -public class TickThread extends Thread { +public abstract class TickThread extends BaseYieldingThread { // Gale - base thread pools public static final boolean STRICT_THREAD_CHECKS = Boolean.getBoolean("paper.strict-thread-checks"); @@ -65,7 +69,7 @@ public class TickThread extends Thread { } private TickThread(final Runnable run, final String name, final int id) { - super(run, name); + super(run, name, ServerThreadPool.taskQueues, ServerThreadPool.threadsWaitingForYieldingTasks, ServerThreadPool.threadsWaitingForFreeTasks, true, 1); // Gale - base thread pools this.id = id; } diff --git a/src/main/java/io/papermc/paper/world/ThreadedWorldUpgrader.java b/src/main/java/io/papermc/paper/world/ThreadedWorldUpgrader.java index 95cac7edae8ac64811fc6a2f6b97dd4a0fceb0b0..e0220511648861ef9363f260814da69d216f781d 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 pools - remove world upgrade executors + this.threadPool = BaseTaskQueues.scheduledAsync.yieldingExecutor; + /* + // Gale end - base thread pools - 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 pools - 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..2a7f2a410a0622d8c4bf45533ecb549a73df153e 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 pools } diff --git a/src/main/java/net/minecraft/Util.java b/src/main/java/net/minecraft/Util.java index 5ef58831a857fd8aa4ac30147762dc17d773a53e..352ed1db3399bc9453286e28c84f61b6325de78b 100644 --- a/src/main/java/net/minecraft/Util.java +++ b/src/main/java/net/minecraft/Util.java @@ -26,9 +26,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.ConcurrentHashMap; 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 pools - 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 f25b9330e068c7d9e12cb57a7761cfef9ebaf7bc..c20f793c4792b5f06a90d305671ac6d473253644 100644 --- a/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java +++ b/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java @@ -152,10 +152,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 pools - 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 27d4aa45e585842c04491839826d405d6f447f0e..f36a0e29f2ba464eee3fabba0b98a714a3b8a49d 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 pools } - 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 pools if (!engine.isSameThread()) { engine.execute(() -> { // Paper - Fix preemptive player kick on a server shutdown. 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 0c4c62674b4c7e8e3921c7eb3ef726759ac75075..31b488a3c1b81b99bf5bda9f90c3cf697d727841 100644 --- a/src/main/java/net/minecraft/server/Main.java +++ b/src/main/java/net/minecraft/server/Main.java @@ -1,27 +1,22 @@ package net.minecraft.server; -import com.mojang.authlib.GameProfile; -import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService; import com.mojang.datafixers.DataFixer; import com.mojang.datafixers.util.Pair; import com.mojang.logging.LogUtils; import com.mojang.serialization.DynamicOps; -import com.mojang.serialization.Lifecycle; + import java.awt.GraphicsEnvironment; import java.io.File; import java.net.Proxy; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Optional; -import java.util.UUID; import java.util.function.BooleanSupplier; + +import com.mojang.serialization.Lifecycle; import io.papermc.paper.world.ThreadedWorldUpgrader; -import joptsimple.NonOptionArgumentSpec; -import joptsimple.OptionParser; import joptsimple.OptionSet; -import joptsimple.OptionSpec; import net.minecraft.CrashReport; -import net.minecraft.DefaultUncaughtExceptionHandler; import net.minecraft.SharedConstants; import net.minecraft.Util; import net.minecraft.commands.Commands; @@ -57,6 +52,8 @@ import net.minecraft.world.level.storage.LevelStorageSource; import net.minecraft.world.level.storage.LevelSummary; import net.minecraft.world.level.storage.PrimaryLevelData; import net.minecraft.world.level.storage.WorldData; +import org.galemc.gale.executor.queue.BaseTaskQueues; +import org.galemc.gale.executor.thread.pooled.AsyncThreadPool; import org.slf4j.Logger; // CraftBukkit start @@ -64,7 +61,7 @@ import com.google.common.base.Charsets; import com.mojang.bridge.game.PackType; import java.io.InputStreamReader; import java.util.concurrent.atomic.AtomicReference; -import net.minecraft.SharedConstants; + import org.bukkit.configuration.file.YamlConfiguration; // CraftBukkit end @@ -228,6 +225,15 @@ public class Main { WorldStem worldstem; + // Gale start - base thread pools + // Initialize the task queues by calling an arbitrary method on the last queue + //noinspection ResultOfMethodCallIgnored + BaseTaskQueues.scheduledAsync.hashCode(); + // Initialize the async executor by calling an arbitrary method + //noinspection ResultOfMethodCallIgnored + AsyncThreadPool.instance.hashCode(); + // Gale end - base thread pools + try { WorldLoader.InitConfig worldloader_c = Main.loadOrCreateConfig(dedicatedserversettings.getProperties(), convertable_conversionsession, flag, resourcepackrepository); diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java index eb951c9fda85d9620d3038a3db22d578db45e878..60ed76588347f4d4c09d8df4952bf55501ed7c00 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -40,7 +40,6 @@ 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.AtomicReference; import java.util.function.BooleanSupplier; import java.util.function.Consumer; @@ -120,7 +119,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; @@ -161,7 +159,15 @@ 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.galemc.gale.executor.MinecraftServerBlockableEventLoop; import org.galemc.gale.configuration.GaleConfigurations; +import org.galemc.gale.executor.annotation.thread.OriginalServerThreadOnly; +import org.galemc.gale.executor.queue.BaseTaskQueues; +import org.galemc.gale.executor.queue.ScheduledServerThreadTaskQueues; +import org.galemc.gale.executor.thread.OriginalServerThread; +import org.galemc.gale.executor.thread.pooled.AsyncThreadPool; +import org.galemc.gale.executor.thread.wait.SignalReason; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; // CraftBukkit start @@ -192,11 +198,19 @@ 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 pools public static final int SERVER_THREAD_PRIORITY = Integer.getInteger("gale.thread.priority.server", 8); // Gale - server thread priority environment variable - private static MinecraftServer SERVER; // Paper + // Gale start - base thread pools + public static MinecraftServer SERVER; // Paper // Gale - base thread pools - private -> public + + /** + * Whether {@link #SERVER} has been set. + */ + public static boolean isConstructed; + + // Gale end - base thread pools public static final Logger LOGGER = LogUtils.getLogger(); public static final String VANILLA_BRAND = "vanilla"; private static final float AVERAGE_TICK_TIME_SMOOTHING = 0.8F; @@ -226,6 +240,10 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop registries; private Map, ServerLevel> levels; + // Gale start - base thread pools - optimize server levels + private @NotNull ServerLevel @NotNull [] levelArray = ArrayConstants.emptyServerLevelArray; + private @Nullable ServerLevel overworld; + // Gale end - base thread pools - optimize server levels private PlayerList playerList; private volatile boolean running; private volatile boolean isRestarting = false; // Paper - flag to signify we're attempting to restart @@ -255,10 +273,115 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop static, final -> non-final (but still effectively final) + // Gale start - base thread pools - make fields volatile + private volatile long nextTickTime; + private volatile long delayedTasksMaxNextTickTime; + // Gale end - base thread pools - make fields volatile + + // Gale start - base thread pools + + public static volatile long nextTickStartNanoTime; + public static volatile long delayedTasksMaxNextTickNanoTime; + + /** + * Sets {@link #nextTickTime}, and sets {@link #nextTickStartNanoTime} accordingly. + */ + private void setNextTickTime(long nextTickTime) { + this.nextTickTime = nextTickTime; + /* + Add 10000 nanoseconds, to make sure the currentTime() >= nextTickTime check will be true after this moment + regardless of the nanosecond granularity of the Condition#await function, which is probably somewhere around + 26 nanoseconds. + */ + nextTickStartNanoTime = 1_000_000L * this.nextTickTime + 10_000L; + } + + /** + * Sets {@link #delayedTasksMaxNextTickTime}, and sets {@link #delayedTasksMaxNextTickNanoTime} accordingly. + * + * @see #setNextTickTime + */ + private void setDelayedTasksMaxNextTickTime(long delayedTasksMaxNextTickTime) { + this.delayedTasksMaxNextTickTime = delayedTasksMaxNextTickTime; + delayedTasksMaxNextTickNanoTime = 1_000_000L * this.delayedTasksMaxNextTickTime + 10_000L; + } + + /** + * Whether to skip the next call to {@link #mayHaveDelayedTasks()} and simply return true. + * This is typically set to true when a new task is added to a queue with tasks that count as potentially + * delayed tasks, or when an element from such a queue is successfully polled (even though it may afterwards be + * empty, it seems better to simply poll again next time, rather than perform the full {@link #mayHaveDelayedTasks()} + * check that loops over all queues). + */ + public static volatile boolean nextTimeAssumeWeMayHaveDelayedTasks; + + /** + * Whether the value of {@link #lastComputedMayHaveDelayedTasks} should be assumed to be correct. + */ + public static volatile boolean mayHaveDelayedTasksIsCurrentlyComputed; + + /** + * The cached last computed correct (except for potential race condition mistakes in the computation) + * value of {@link #mayHaveDelayedTasks()}. + */ + public static volatile boolean lastComputedMayHaveDelayedTasks; + + /** + * Whether the server is currently in spare time after a tick. + * This is set to true by the {@link #serverThread} when entering the spare time phase, + * either at the end of a tick, or at the start of one (if it occurred too early), and set to false after + * the corresponding {@link #managedBlock} call. + */ + public static volatile boolean isInSpareTime = false; + + /** + * Whether the server is currently waiting for the next tick, which is one of the cases where + * {@link #isInSpareTime} is true. Specifically, the other case where {@link #isInSpareTime} is true is + * while {@link #isOversleep} is true. + */ + public static volatile boolean isWaitingUntilNextTick = false; + + /** + * A potentially out-of-date value indicating whether {@link #isInSpareTime} is true + * and {@link #haveTime()} is false and {@link #blockingCount} is 0. + * This should be updated just in time before it is potentially needed. + */ + public static volatile boolean isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking = false; + + /** + * The stop condition provided to the current call of {@link #managedBlock}, or null if no {@link #managedBlock} + * call is ongoing. + */ + public static volatile @Nullable BooleanSupplier currentManagedBlockStopCondition; + + /** + * Whether the {@link #currentManagedBlockStopCondition} has become true + * during the last {@link #managedBlock} call. + */ + public static volatile boolean currentManagedBlockStopConditionHasBecomeTrue = false; + + public static final SignalReason managedBlockStopConditionBecameTrueSignalReason = SignalReason.createNonRetrying(); + + public static void signalServerThreadIfCurrentManagedBlockStopConditionBecameTrue() { + if (currentManagedBlockStopConditionHasBecomeTrue) { + // We already signalled the thread + return; + } + if (currentManagedBlockStopCondition == null) { + // There is no ongoing managedBlock cal + return; + } + if (!currentManagedBlockStopCondition.getAsBoolean()) { + // The stop condition is not true + return; + } + currentManagedBlockStopConditionHasBecomeTrue = true; + serverThread.signal(managedBlockStopConditionBecameTrueSignalReason); + } + + // Gale start - base thread pools + private final PackRepository packRepository; private final ServerScoreboard scoreboard; @Nullable @@ -287,7 +410,7 @@ 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 pools - make fields volatile // CraftBukkit end // Spigot start public static final int TPS = 20; @@ -303,9 +426,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop S spin(Function serverFactory) { + public static S spin(Function serverFactory) { // Gale - base thread pools AtomicReference atomicreference = new AtomicReference(); - Thread thread = new io.papermc.paper.util.TickThread(() -> { // Paper - rewrite chunk system + OriginalServerThread thread = new OriginalServerThread(() -> { // Paper - rewrite chunk system // Gale - base thread pools ((MinecraftServer) atomicreference.get()).runServer(); }, "Server thread"); @@ -324,16 +447,19 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop holdergetter = this.registries.compositeAccess().registryOrThrow(Registries.BLOCK).asLookup().filterFeatures(this.worldData.enabledFeatures()); this.structureTemplateManager = new StructureTemplateManager(worldstem.resourceManager(), convertable_conversionsession, datafixer, holdergetter); - this.serverThread = thread; + // Gale start - base thread pools + serverThread = thread; + AsyncThreadPool.instance.intendedActiveThreadCountMayHaveChanged(); + // Gale end - base thread pools this.executor = Util.backgroundExecutor(); } // CraftBukkit start @@ -599,7 +728,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop public public volatile boolean hasFullyShutdown = false; // Paper private boolean hasLoggedStop = false; // Paper private final Object stopLock = new Object(); @@ -916,8 +1046,10 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop Util.getMillis() - startTime >= 30 || !BaseTaskQueues.scheduledAsync.hasTasks(), null); // Paper + LOGGER.info("Shutting down IO executor..."); + // Gale end - base thread pools - remove Paper async executor + // Gale end - base thread pools - remove background executor Util.shutdownExecutors(); // Paper LOGGER.info("Closing Server"); try { @@ -1017,7 +1148,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop public // Paper start if (this.forceTicks) { return true; @@ -1253,13 +1424,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop public private boolean canOversleep() { - return this.mayHaveDelayedTasks && Util.getMillis() < this.delayedTasksMaxNextTickTime; + return Util.getMillis() < this.delayedTasksMaxNextTickTime && mayHaveDelayedTasks(); // Gale - base thread pools } private boolean canSleepForTickNoOversleep() { @@ -1268,7 +1439,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { return !this.canSleepForTickNoOversleep(); // Paper - move oversleep into full server tick + // Gale start - base thread pools }); + isInSpareTime = false; + isWaitingUntilNextTick = false; + // Gale end - base thread pools 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 - super.doRunTask(ticktask); - } - private void updateStatusIcon(ServerStatus metadata) { Optional optional = Optional.of(this.getFile("server-icon.png")).filter(File::isFile); @@ -1378,14 +1508,19 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { return !this.canOversleep(); + // Gale start - base thread pools }); + isInSpareTime = false; + // Gale end - base thread pools isOversleep = false;MinecraftTimings.serverOversleep.stopTiming(); // Paper end new com.destroystokyo.paper.event.server.ServerTickStartEvent(this.tickCount+1).callEvent(); // Paper ++this.tickCount; + ScheduledServerThreadTaskQueues.shiftTasksForNextTick(); // Gale - base thread pools this.tickChildren(shouldKeepTicking); if (i - this.lastServerStatus >= 5000000000L) { this.lastServerStatus = i; @@ -1420,7 +1555,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 pools - optimize server levels if (level.paperConfig().chunks.autoSaveInterval.value() > 0) { level.saveIncrementally(fullSave); } @@ -1432,7 +1567,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 @@ -1569,7 +1702,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 pools - 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 pools - optimize server levels } public void removeLevel(ServerLevel level) { @@ -1598,6 +1743,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 pools - 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 pools - optimize server levels } // CraftBukkit end @@ -1605,8 +1758,14 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop getAllLevels() { - return this.levels.values(); + return this.levels == null ? Collections.emptyList() : this.levels.values(); // Gale - base thread pools } public String getServerVersion() { @@ -1726,10 +1885,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop players; public final ServerChunkCache chunkSource; + // Gale start - base thread pools + @AnyThreadSafe(Access.READ) + public volatile int serverLevelArrayIndex; + // Gale end - base thread pools private final MinecraftServer server; public final PrimaryLevelData serverLevelData; // CraftBukkit - type final EntityTickList entityTickList; @@ -2558,7 +2566,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 pools - 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 68e0f2208c5f098042ebfad08301e3154e2a2152..deadeb7a98e5b64d7b9fae3a9e7858a4cd1d39e2 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.VoxelShape; import org.bukkit.craftbukkit.util.permissions.CraftDefaultPermissions; 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.ScheduledServerThreadTaskQueues; import org.slf4j.Logger; // CraftBukkit start @@ -552,7 +554,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic Objects.requireNonNull(this.connection); // CraftBukkit - Don't wait - minecraftserver.scheduleOnMain(networkmanager::handleDisconnection); // Paper + ScheduledServerThreadTaskQueues.add(networkmanager::handleDisconnection, ScheduledServerThreadTaskQueues.HANDLE_DISCONNECT_TASK_MAX_DELAY); // Paper // Gale - base thread pools } private CompletableFuture filterTextPacket(T text, BiFunction> filterer) { @@ -883,21 +885,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 pools - 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 + ScheduledServerThreadTaskQueues.add(() -> this.disconnect(Component.translatable("disconnect.spam", ArrayConstants.emptyObjectArray), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM), ScheduledServerThreadTaskQueues.KICK_FOR_COMMAND_PACKET_SPAM_TASK_MAX_DELAY); // Paper - kick event cause // Gale - JettPack - reduce array allocations // Gale - base thread pools 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 + ScheduledServerThreadTaskQueues.add(() -> this.disconnect(Component.translatable("disconnect.spam", ArrayConstants.emptyObjectArray), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM), ScheduledServerThreadTaskQueues.KICK_FOR_COMMAND_PACKET_SPAM_TASK_MAX_DELAY); // Paper - kick event cause // Gale - JettPack - reduce array allocations // Gale - base thread pools return; } // Paper end @@ -922,7 +923,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic if (!event.isHandled()) { if (!event.isCancelled()) { - this.server.scheduleOnMain(() -> { // This needs to be on main + ScheduledServerThreadTaskQueues.add(() -> { // This needs to be on main // Gale - base thread pools ParseResults parseresults = this.server.getCommands().getDispatcher().parse(stringreader, this.player.createCommandSourceStack()); this.server.getCommands().getDispatcher().getCompletionSuggestions(parseresults).thenAccept((suggestions) -> { @@ -933,7 +934,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic this.connection.send(new ClientboundCommandSuggestionsPacket(packet.getId(), suggestEvent.getSuggestions())); // Paper end - Brigadier API }); - }); + }, ScheduledServerThreadTaskQueues.SEND_COMMAND_COMPLETION_SUGGESTIONS_TASK_MAX_DELAY); // Gale - base thread pools } } else if (!completions.isEmpty()) { final com.mojang.brigadier.suggestion.SuggestionsBuilder builder0 = new com.mojang.brigadier.suggestion.SuggestionsBuilder(command, stringreader.getTotalLength()); @@ -1247,7 +1248,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 + ScheduledServerThreadTaskQueues.add(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION), ScheduledServerThreadTaskQueues.KICK_FOR_BOOK_TOO_LARGE_PACKET_TASK_MAX_DELAY); // Paper - kick event cause // Gale - base thread pools return; } byteTotal += byteLength; @@ -1270,14 +1271,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 + ScheduledServerThreadTaskQueues.add(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION), ScheduledServerThreadTaskQueues.KICK_FOR_BOOK_TOO_LARGE_PACKET_TASK_MAX_DELAY); // Paper - kick event cause // Gale - base thread pools 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 + ScheduledServerThreadTaskQueues.add(() -> this.disconnect("Book edited too quickly!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION), ScheduledServerThreadTaskQueues.KICK_FOR_EDITING_BOOK_TOO_QUICKLY_TASK_MAX_DELAY); // Paper - kick event cause // Paper - Also ensure this is called on main // Gale - base thread pools return; } this.lastBookTick = MinecraftServer.currentTick; @@ -2081,10 +2082,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 pools - optimize server levels Entity entity = packet.getEntity(worldserver); if (entity != null) { @@ -2233,9 +2231,9 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic } // CraftBukkit end if (ServerGamePacketListenerImpl.isChatMessageIllegal(packet.message())) { - this.server.scheduleOnMain(() -> { // Paper - push to main for event firing + ScheduledServerThreadTaskQueues.add(() -> { // Paper - push to main for event firing // Gale - base thread pools 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 + }, ScheduledServerThreadTaskQueues.KICK_FOR_ILLEGAL_CHARACTERS_IN_CHAT_PACKET_TASK_MAX_DELAY); // Paper - push to main for event firing // Gale - base thread pools } else { Optional optional = this.tryHandleChat(packet.message(), packet.timeStamp(), packet.lastSeenMessages()); @@ -2269,9 +2267,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 + ScheduledServerThreadTaskQueues.add(() -> { // Paper - push to main for event firing // Gale - base thread pools this.disconnect(Component.translatable("multiplayer.disconnect.illegal_characters"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_CHARACTERS); // Paper - }); // Paper - push to main for event firing + }, ScheduledServerThreadTaskQueues.KICK_FOR_ILLEGAL_CHARACTERS_IN_CHAT_PACKET_TASK_MAX_DELAY); // Paper - push to main for event firing // Gale - base thread pools } else { Optional optional = this.tryHandleChat(packet.command(), packet.timeStamp(), packet.lastSeenMessages()); @@ -2353,9 +2351,9 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic private Optional tryHandleChat(String message, Instant timestamp, LastSeenMessages.Update acknowledgment) { if (!this.updateChatOrder(timestamp)) { if (GaleGlobalConfiguration.get().logToConsole.chat.outOfOrderMessageWarning) ServerGamePacketListenerImpl.LOGGER.warn("{} sent out-of-order chat: '{}': {} > {}", this.player.getName().getString(), message, this.lastChatTimeStamp.get().getEpochSecond(), timestamp.getEpochSecond()); // Paper // Gale - do not log out-of-order message warnings - this.server.scheduleOnMain(() -> { // Paper - push to main - this.disconnect(Component.translatable("multiplayer.disconnect.out_of_order_chat"), org.bukkit.event.player.PlayerKickEvent.Cause.OUT_OF_ORDER_CHAT); // Paper - kick event ca - }); // Paper - push to main + ScheduledServerThreadTaskQueues.add(() -> { // Paper - push to main // Gale - base thread pools + this.disconnect(Component.translatable("multiplayer.disconnect.out_of_order_chat"), org.bukkit.event.player.PlayerKickEvent.Cause.OUT_OF_ORDER_CHAT); // Paper - kick event cause + }, ScheduledServerThreadTaskQueues.KICK_FOR_OUT_OF_ORDER_CHAT_PACKET_TASK_MAX_DELAY); // Paper - push to main // Gale - base thread pools return Optional.empty(); } 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)); @@ -3290,7 +3288,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 + ScheduledServerThreadTaskQueues.add(() -> this.disconnect(net.minecraft.network.chat.Component.translatable("disconnect.spam", ArrayConstants.emptyObjectArray), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM), ScheduledServerThreadTaskQueues.KICK_FOR_RECIPE_PACKET_SPAM_TASK_MAX_DELAY); // Paper - kick event cause // Gale - JettPack - reduce array allocations // Gale - base thread pools return; } } diff --git a/src/main/java/net/minecraft/server/network/TextFilterClient.java b/src/main/java/net/minecraft/server/network/TextFilterClient.java index 4b3d2280326c7eeda4952c36edff141cbff90e16..fa3a58f09178604e301b107f1a029e59a7164e13 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 pools - 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 ac12cde39125f3b9dc57f251dd124739422426f9..92a1a5cfc9f0ba2f2af7773e98794ec0e5db81f1 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.EnumSet; @@ -105,10 +104,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.ScheduledServerThreadTaskQueues; 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; @@ -255,7 +254,7 @@ public abstract class PlayerList { // Gale start - MultiPaper - do not place player in world if kicked before being spawned in if (!connection.isConnected() || player.quitReason != null) { - pendingPlayers.remove(player.getUUID(), player); + /*pendingPlayers.remove(player.getUUID(), player);*/ // Gale - base thread pools - this patch was removed from Paper but might be useful later return; } // Gale end - MultiPaper - do not place player in world if kicked before being spawned in @@ -296,6 +295,58 @@ public abstract class PlayerList { player.getRecipeBook().sendInitialRecipeBook(player); this.updateEntireScoreboard(worldserver1.getScoreboard(), player); this.server.invalidateStatus(); +/* // Gale - base thread pools - this patch was removed from Paper but might be useful later + // Paper start - async load spawn in chunk + ServerLevel finalWorldserver = worldserver1; + finalWorldserver.pendingLogin.add(player); + int chunkX = loc.getBlockX() >> 4; + int chunkZ = loc.getBlockZ() >> 4; + final net.minecraft.world.level.ChunkPos pos = new net.minecraft.world.level.ChunkPos(chunkX, chunkZ); + net.minecraft.server.level.ChunkMap playerChunkMap = worldserver1.getChunkSource().chunkMap; + net.minecraft.server.level.DistanceManager distanceManager = playerChunkMap.distanceManager; + io.papermc.paper.chunk.system.ChunkSystem.scheduleTickingState( + worldserver1, chunkX, chunkZ, net.minecraft.server.level.ChunkHolder.FullChunkStatus.ENTITY_TICKING, true, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHEST, + (chunk) -> { + ScheduledServerThreadTaskQueues.add(() -> { // Gale - base thread pools + try { + if (!playerconnection.connection.isConnected()) { + return; + } + PlayerList.this.postChunkLoadJoin( + player, finalWorldserver, connection, playerconnection, + nbttagcompound, s1, lastKnownName + ); + distanceManager.addTicket(net.minecraft.server.level.TicketType.LOGIN, pos, 31, pos.toLong()); + } finally { + finalWorldserver.pendingLogin.remove(player); + } + }, ScheduledServerThreadTaskQueues.POST_CHUNK_LOAD_JOIN_TASK_MAX_DELAY); // Gale - base thread pools + } + ); + } + + public ServerPlayer getActivePlayer(UUID uuid) { + ServerPlayer player = this.playersByUUID.get(uuid); + return player != null ? player : pendingPlayers.get(uuid); + } + + void disconnectPendingPlayer(ServerPlayer entityplayer) { + Component msg = Component.translatable("multiplayer.disconnect.duplicate_login"); + entityplayer.networkManager.send(new net.minecraft.network.protocol.game.ClientboundDisconnectPacket(msg), net.minecraft.network.PacketSendListener.thenRun(() -> { + entityplayer.networkManager.disconnect(msg); + entityplayer.networkManager = null; + })); + } + + private void postChunkLoadJoin(ServerPlayer player, ServerLevel worldserver1, Connection networkmanager, ServerGamePacketListenerImpl playerconnection, CompoundTag nbttagcompound, String s1, String s) { + pendingPlayers.remove(player.getUUID(), player); + if (!networkmanager.isConnected()) { + return; + } + player.didPlayerJoinEvent = true; + // Paper end +*/ // Gale - base thread pools - this patch was removed from Paper but might be useful later MutableComponent ichatmutablecomponent; if (player.getGameProfile().getName().equalsIgnoreCase(s)) { @@ -1507,10 +1558,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 pools - optimize server levels if (worldserver != null) { worldserver.getChunkSource().setViewDistance(viewDistance); @@ -1522,10 +1571,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 pools - 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..321be4cfea7228f5f5131eb521daa67590f00078 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 pools 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 pools 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 pools + public boolean hasPendingTasks() { + return !this.pendingRunnables.isEmpty(); + } + // Gale end - base thread pools + @Override public String name() { return this.name; @@ -102,6 +110,7 @@ public abstract class BlockableEventLoop implements Profiler } + @Override // Gale - base thread pools 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 9948cc4c65d5681c171b38cdf7cf3e63a01e4364..c37793871951b0044168610bc05ee0529f3c4611 100644 --- a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java +++ b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java @@ -98,7 +98,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 pools - 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 e23fdd5ba09b50b7eef0ca4f36c5480779fba624..79f3a6174873834de61d7dc9fdbf6eb5a0fd6cd9 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java @@ -986,7 +986,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 pools - 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)) @@ -1170,7 +1170,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 pools - 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"); @@ -2526,7 +2526,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 pools - 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 f8d321e925bf2708e51590542325c1bdc67d5964..a190bb9ce7b3701963f315452359f6f9c3aae329 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; @@ -20,7 +19,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; @@ -114,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; @@ -134,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.ScheduledServerThreadTaskQueues; public class CraftWorld extends CraftRegionAccessor implements World { public static final int CUSTOM_DIMENSION_OFFSET = 10; @@ -2356,11 +2354,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(() -> { + ScheduledServerThreadTaskQueues.add(() -> { // Gale - base thread pools 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()); - }); + }, ScheduledServerThreadTaskQueues.COMPLETE_CHUNK_FUTURE_TASK_MAX_DELAY); // Gale - base thread pools }); 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 78f53ee557276de85f0431ebcb146445b1f4fb92..c8b0a191832523e6c2e0fe4fd6cb1b8fa5104f86 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java @@ -190,6 +190,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.ScheduledServerThreadTaskQueues; public abstract class CraftEntity implements org.bukkit.entity.Entity { private static PermissibleBase perm; @@ -1280,7 +1281,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(() -> { + ScheduledServerThreadTaskQueues.add(() -> { // Gale - base thread pools try { ret.complete(CraftEntity.this.teleport(locationClone, cause) ? Boolean.TRUE : Boolean.FALSE); } catch (Throwable throwable) { @@ -1290,7 +1291,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); } - }); + }, ScheduledServerThreadTaskQueues.TELEPORT_ASYNC_TASK_MAX_DELAY); // Gale - base thread pools }); 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 65ec8cf910575dfa4c5024ec69b3be1ef2634722..174c248aa706f6b5f3e248cb7604b44a4d508967 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 2e31501d26b141729c80975e97a23b09653ba3bf..5a454236073dd75ed36d058c0f033c4aada403e3 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 diff --git a/src/main/java/org/galemc/gale/configuration/GaleConfigurations.java b/src/main/java/org/galemc/gale/configuration/GaleConfigurations.java index 69acbab61a79c24312359a63086f9353d740113f..49ace73d901b6f55545bb21a93d026a04c5757ad 100644 --- a/src/main/java/org/galemc/gale/configuration/GaleConfigurations.java +++ b/src/main/java/org/galemc/gale/configuration/GaleConfigurations.java @@ -267,7 +267,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 (!ScheduledServerThreadTaskQueues.writeLock.tryLock()); + try { + // Update the values in MinecraftServerBlockableEventLoop for quick access + ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY = this.defaultValue >= 0 ? this.defaultValue : 2; + ScheduledServerThreadTaskQueues.COMPLETE_CHUNK_FUTURE_TASK_MAX_DELAY = this.completeChunkFuture >= 0 ? this.completeChunkFuture : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledServerThreadTaskQueues.POST_CHUNK_LOAD_JOIN_TASK_MAX_DELAY = this.postChunkLoadJoin >= 0 ? this.postChunkLoadJoin : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledServerThreadTaskQueues.ANTI_XRAY_MODIFY_BLOCKS_TASK_MAX_DELAY = this.antiXrayModifyBlocks >= 0 ? this.antiXrayModifyBlocks : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledServerThreadTaskQueues.TELEPORT_ASYNC_TASK_MAX_DELAY = this.teleportAsync >= 0 ? this.teleportAsync : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledServerThreadTaskQueues.SEND_COMMAND_COMPLETION_SUGGESTIONS_TASK_MAX_DELAY = this.sendCommandCompletionSuggestions >= 0 ? this.sendCommandCompletionSuggestions : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledServerThreadTaskQueues.KICK_FOR_COMMAND_PACKET_SPAM_TASK_MAX_DELAY = this.kickForCommandPacketSpam >= 0 ? this.kickForCommandPacketSpam : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledServerThreadTaskQueues.KICK_FOR_RECIPE_PACKET_SPAM_TASK_MAX_DELAY = this.kickForRecipePacketSpam >= 0 ? this.kickForRecipePacketSpam : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledServerThreadTaskQueues.KICK_FOR_BOOK_TOO_LARGE_PACKET_TASK_MAX_DELAY = this.kickForBookTooLargePacket >= 0 ? this.kickForBookTooLargePacket : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledServerThreadTaskQueues.KICK_FOR_EDITING_BOOK_TOO_QUICKLY_TASK_MAX_DELAY = this.kickForEditingBookTooQuickly >= 0 ? this.kickForEditingBookTooQuickly : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledServerThreadTaskQueues.KICK_FOR_ILLEGAL_CHARACTERS_IN_CHAT_PACKET_TASK_MAX_DELAY = this.kickForIllegalCharactersInChatPacket >= 0 ? this.kickForIllegalCharactersInChatPacket : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledServerThreadTaskQueues.KICK_FOR_OUT_OF_ORDER_CHAT_PACKET_TASK_MAX_DELAY = this.kickForOutOfOrderChatPacket >= 0 ? this.kickForOutOfOrderChatPacket : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledServerThreadTaskQueues.HANDLE_DISCONNECT_TASK_MAX_DELAY = this.handleDisconnect >= 0 ? this.handleDisconnect : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + // Change the length of the pendingRunnables array of queues + int maxDelay = 0; + for (int delay : new int[]{ + ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY, + ScheduledServerThreadTaskQueues.COMPLETE_CHUNK_FUTURE_TASK_MAX_DELAY, + ScheduledServerThreadTaskQueues.POST_CHUNK_LOAD_JOIN_TASK_MAX_DELAY, + ScheduledServerThreadTaskQueues.ANTI_XRAY_MODIFY_BLOCKS_TASK_MAX_DELAY, + ScheduledServerThreadTaskQueues.TELEPORT_ASYNC_TASK_MAX_DELAY, + ScheduledServerThreadTaskQueues.SEND_COMMAND_COMPLETION_SUGGESTIONS_TASK_MAX_DELAY, + ScheduledServerThreadTaskQueues.KICK_FOR_COMMAND_PACKET_SPAM_TASK_MAX_DELAY, + ScheduledServerThreadTaskQueues.KICK_FOR_RECIPE_PACKET_SPAM_TASK_MAX_DELAY, + ScheduledServerThreadTaskQueues.KICK_FOR_BOOK_TOO_LARGE_PACKET_TASK_MAX_DELAY, + ScheduledServerThreadTaskQueues.KICK_FOR_EDITING_BOOK_TOO_QUICKLY_TASK_MAX_DELAY, + ScheduledServerThreadTaskQueues.KICK_FOR_ILLEGAL_CHARACTERS_IN_CHAT_PACKET_TASK_MAX_DELAY, + ScheduledServerThreadTaskQueues.KICK_FOR_OUT_OF_ORDER_CHAT_PACKET_TASK_MAX_DELAY, + ScheduledServerThreadTaskQueues.HANDLE_DISCONNECT_TASK_MAX_DELAY + }) { + if (delay > maxDelay) { + maxDelay = delay; + } + } + int newPendingRunnablesLength = maxDelay + 1; + int oldPendingRunnablesLength = ScheduledServerThreadTaskQueues.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 < ScheduledServerThreadTaskQueues.queues.length; i++) { + ScheduledServerThreadTaskQueues.queues[maxDelay].addAll(ScheduledServerThreadTaskQueues.queues[i]); + } + // Update the first queue with elements index + if (ScheduledServerThreadTaskQueues.firstQueueWithPotentialTasksIndex >= newPendingRunnablesLength) { + ScheduledServerThreadTaskQueues.firstQueueWithPotentialTasksIndex = maxDelay; + } + } + ScheduledServerThreadTaskQueues.queues = Arrays.copyOf(ScheduledServerThreadTaskQueues.queues, newPendingRunnablesLength); + if (newPendingRunnablesLength > oldPendingRunnablesLength) { + // Create new queues + for (int i = oldPendingRunnablesLength; i < newPendingRunnablesLength; i++) { + ScheduledServerThreadTaskQueues.queues[i] = new MultiThreadedQueue<>(); + } + } + } + } finally { + ScheduledServerThreadTaskQueues.writeLock.unlock(); + } + } + + } + // Gale end - base thread pools + } public GameplayMechanics gameplayMechanics; 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..e73bf57a9777488dc00efe671cee955e6fb108a1 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/AbstractBlockableEventLoop.java @@ -0,0 +1,20 @@ +// Gale - base thread pools + +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..f249ef5a23e85770db224ed0b6f27598e78c5746 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/MinecraftServerBlockableEventLoop.java @@ -0,0 +1,188 @@ +// Gale - base thread pools + +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.thread.ServerThreadOnly; +import org.galemc.gale.executor.queue.BaseTaskQueues; +import org.galemc.gale.executor.queue.ScheduledServerThreadTaskQueues; +import org.galemc.gale.executor.thread.pooled.ServerThread; +import org.jetbrains.annotations.NotNull; +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 || Thread.currentThread() != ServerThread.getInstance()) && !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 (Thread.currentThread() != ServerThread.getInstance()) { + this.submitAsync(runnable).join(); + } else { + runnable.run(); + } + } + + /** + * @deprecated Use {@link ScheduledServerThreadTaskQueues#add(Runnable, int)} instead: + * do not rely on {@link ScheduledServerThreadTaskQueues#DEFAULT_TASK_MAX_DELAY}. + */ + @Deprecated + @Override + public void tell(@NotNull Runnable message) { + ScheduledServerThreadTaskQueues.add(() -> { + //noinspection NonAtomicOperationOnVolatileField + ++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 { + //noinspection NonAtomicOperationOnVolatileField + --reentrantCount; + if (MinecraftServer.isWaitingUntilNextTick) { + MinecraftServer.signalServerThreadIfCurrentManagedBlockStopConditionBecameTrue(); + } + } + 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 Thread.currentThread() == MinecraftServer.serverThread; + } + + @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. + */ + @ServerThreadOnly + 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 = ScheduledServerThreadTaskQueues.poll(ServerThread.getInstance(), 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. + */ + @ServerThreadOnly + 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.anyTickScheduledServerThread.poll(ServerThread.getInstance()); + if (task == null) { + break; + } + task.run(); + } + } + + @ServerThreadOnly + public void managedBlock(@NotNull BooleanSupplier stopCondition) { + MinecraftServer.currentManagedBlockStopCondition = stopCondition; + try { + // Check stop condition beforehand to prevent unnecessarily releasing main thread + MinecraftServer.currentManagedBlockStopConditionHasBecomeTrue = false; + if (stopCondition.getAsBoolean()) { + MinecraftServer.currentManagedBlockStopConditionHasBecomeTrue = true; + return; + } + //noinspection NonAtomicOperationOnVolatileField + ++blockingCount; + try { + MinecraftServer.serverThread.runTasksUntil(stopCondition, null); + } finally { + //noinspection NonAtomicOperationOnVolatileField + --blockingCount; + } + } finally { + MinecraftServer.currentManagedBlockStopCondition = null; + } + } + + @Override + public @NotNull String name() { + return NAME; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/annotation/PotentiallyBlocking.java b/src/main/java/org/galemc/gale/executor/annotation/PotentiallyBlocking.java index 71f26852c96dea34ea07efe07f834f8262509957..d324c303245bcbedaaaab573803d73caff941901 100644 --- a/src/main/java/org/galemc/gale/executor/annotation/PotentiallyBlocking.java +++ b/src/main/java/org/galemc/gale/executor/annotation/PotentiallyBlocking.java @@ -2,7 +2,7 @@ package org.galemc.gale.executor.annotation; -import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.AbstractYieldingThread; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; @@ -19,7 +19,7 @@ import java.lang.annotation.Target; * {@link PotentiallyBlocking}, {@link PotentiallyYielding} or {@link YieldFree} may all be used. *
    * Methods that are potentially blocking, including those annotated with {@link PotentiallyBlocking}, must never - * be called on a {@link BaseThread}. + * be called on an {@link AbstractYieldingThread}. * * @author Martijn Muijsers under AGPL-3.0 */ 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..7ff4e4ab43d316e319efb33b2dd365d679a58118 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.AbstractYieldingThread; + 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 AbstractYieldingThread}. * * @author Martijn Muijsers under AGPL-3.0 */ diff --git a/src/main/java/org/galemc/gale/executor/annotation/thread/AsyncThreadOnly.java b/src/main/java/org/galemc/gale/executor/annotation/thread/AsyncThreadOnly.java new file mode 100644 index 0000000000000000000000000000000000000000..604ece0c20e986afdf6958ba969052b2c69762b7 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/annotation/thread/AsyncThreadOnly.java @@ -0,0 +1,36 @@ +// Gale - thread-safety annotations + +package org.galemc.gale.executor.annotation.thread; + +import org.galemc.gale.executor.annotation.Access; +import org.galemc.gale.executor.annotation.PotentiallyBlocking; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * An annotation primarily for methods, identifying methods that can only be called on a thread that is an instance + * of {@link AsyncThread}. + *
    + * This annotation can also be used on fields or classes, similar to {@link ThreadRestricted}. + *
    + * In a method annotated with {@link AsyncThreadOnly}, fields and methods annotated with + * {@link AsyncThreadOnly}, {@link BaseYieldingThreadOnly}, {@link YieldingThreadOnly} + * or {@link AnyThreadSafe} may be used. + *
    + * Methods that are annotated with {@link AsyncThreadOnly} must never call methods that are annotated with + * {@link PotentiallyBlocking}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@Documented +@Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD}) +public @interface AsyncThreadOnly { + + /** + * @see ThreadRestricted#fieldAccess() + */ + Access value() default Access.READ_WRITE; + +} diff --git a/src/main/java/org/galemc/gale/executor/annotation/thread/BaseYieldingThreadOnly.java b/src/main/java/org/galemc/gale/executor/annotation/thread/BaseYieldingThreadOnly.java new file mode 100644 index 0000000000000000000000000000000000000000..2b20fd3fd5e9d73049bcc6bf0cbca0eb7a77630b --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/annotation/thread/BaseYieldingThreadOnly.java @@ -0,0 +1,36 @@ +// Gale - thread-safety annotations + +package org.galemc.gale.executor.annotation.thread; + +import org.galemc.gale.executor.annotation.Access; +import org.galemc.gale.executor.annotation.PotentiallyBlocking; +import org.galemc.gale.executor.thread.BaseYieldingThread; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * An annotation primarily for methods, identifying methods that can only be called on a thread that is an instance + * of {@link BaseYieldingThread}. + *
    + * This annotation can also be used on fields or classes, similar to {@link ThreadRestricted}. + *
    + * In a method annotated with {@link BaseYieldingThreadOnly}, fields and methods annotated with + * {@link BaseYieldingThreadOnly}, {@link YieldingThreadOnly} or {@link AnyThreadSafe} may be used. + *
    + * Methods that are annotated with {@link BaseYieldingThreadOnly} must never call methods that are annotated with + * {@link PotentiallyBlocking}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@Documented +@Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD}) +public @interface BaseYieldingThreadOnly { + + /** + * @see ThreadRestricted#fieldAccess() + */ + Access value() default Access.READ_WRITE; + +} diff --git a/src/main/java/org/galemc/gale/executor/annotation/thread/OriginalServerThreadOnly.java b/src/main/java/org/galemc/gale/executor/annotation/thread/OriginalServerThreadOnly.java new file mode 100644 index 0000000000000000000000000000000000000000..13bd7088eb33cfb8e839debcb77f3a26c2d2a441 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/annotation/thread/OriginalServerThreadOnly.java @@ -0,0 +1,35 @@ +// Gale - thread-safety annotations + +package org.galemc.gale.executor.annotation.thread; + +import org.galemc.gale.executor.annotation.Access; +import org.galemc.gale.executor.annotation.PotentiallyBlocking; +import org.galemc.gale.executor.thread.OriginalServerThread; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * An annotation primarily for methods, identifying methods that can only be called from the + * {@link OriginalServerThread}. + *
    + * This annotation can also be used on fields, similar to {@link ThreadRestricted}. + *
    + * Methods that are annotated with {@link OriginalServerThreadOnly} must never call methods that are annotated with + * {@link PotentiallyBlocking}. + * + * @see ThreadRestricted + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@Documented +@Target({ElementType.METHOD, ElementType.FIELD}) +public @interface OriginalServerThreadOnly { + + /** + * @see ThreadRestricted#fieldAccess() + */ + Access value() default Access.READ_WRITE; + +} diff --git a/src/main/java/org/galemc/gale/executor/annotation/thread/ServerThreadOnly.java b/src/main/java/org/galemc/gale/executor/annotation/thread/ServerThreadOnly.java new file mode 100644 index 0000000000000000000000000000000000000000..efd540e2a40d79f70e7ac6a709bb10f0138ed7a8 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/annotation/thread/ServerThreadOnly.java @@ -0,0 +1,38 @@ +// Gale - thread-safety annotations + +package org.galemc.gale.executor.annotation.thread; + +import org.galemc.gale.executor.annotation.Access; +import org.galemc.gale.executor.annotation.PotentiallyBlocking; +import org.galemc.gale.executor.thread.pooled.ServerThread; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * An annotation primarily for methods, identifying methods that can only be called from a {@link ServerThread}. + *
    + * This annotation can also be used on fields or classes, similar to {@link ThreadRestricted}. + *
    + * In a method annotated with {@link ServerThreadOnly}, fields and methods annotated with + * {@link ServerThreadOnly}, {@link BaseYieldingThreadOnly}, {@link YieldingThreadOnly} + * or {@link AnyThreadSafe} may be used. + *
    + * Methods that are annotated with {@link ServerThreadOnly} must never call methods that are annotated with + * {@link PotentiallyBlocking}. + * + * @see ThreadRestricted + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@Documented +@Target({ElementType.METHOD, ElementType.FIELD}) +public @interface ServerThreadOnly { + + /** + * @see ThreadRestricted#fieldAccess() + */ + Access value() default Access.READ_WRITE; + +} diff --git a/src/main/java/org/galemc/gale/executor/annotation/thread/TickAssistThreadOnly.java b/src/main/java/org/galemc/gale/executor/annotation/thread/TickAssistThreadOnly.java new file mode 100644 index 0000000000000000000000000000000000000000..b86526dbdd531827fc4064b22c3281b1215aa188 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/annotation/thread/TickAssistThreadOnly.java @@ -0,0 +1,37 @@ +// Gale - thread-safety annotations + +package org.galemc.gale.executor.annotation.thread; + +import org.galemc.gale.executor.annotation.Access; +import org.galemc.gale.executor.annotation.PotentiallyBlocking; +import org.galemc.gale.executor.thread.pooled.TickAssistThread; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * An annotation primarily for methods, identifying methods that can only be called on a thread that is an instance + * of {@link TickAssistThread}. + *
    + * This annotation can also be used on fields or classes, similar to {@link ThreadRestricted}. + *
    + * In a method annotated with {@link TickAssistThreadOnly}, fields and methods annotated with + * {@link TickAssistThreadOnly}, {@link BaseYieldingThreadOnly}, {@link YieldingThreadOnly} + * or {@link AnyThreadSafe} may be used. + *
    + * Methods that are annotated with {@link TickAssistThreadOnly} must never call methods that are annotated with + * {@link PotentiallyBlocking}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@Documented +@Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD}) +public @interface TickAssistThreadOnly { + + /** + * @see ThreadRestricted#fieldAccess() + */ + Access value() default Access.READ_WRITE; + +} diff --git a/src/main/java/org/galemc/gale/executor/annotation/thread/YieldingThreadOnly.java b/src/main/java/org/galemc/gale/executor/annotation/thread/YieldingThreadOnly.java new file mode 100644 index 0000000000000000000000000000000000000000..e37018a92d8854674b12172af33073e39982b3a6 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/annotation/thread/YieldingThreadOnly.java @@ -0,0 +1,36 @@ +// Gale - thread-safety annotations + +package org.galemc.gale.executor.annotation.thread; + +import org.galemc.gale.executor.annotation.Access; +import org.galemc.gale.executor.annotation.PotentiallyBlocking; +import org.galemc.gale.executor.thread.AbstractYieldingThread; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * An annotation primarily for methods, identifying methods that can only be called on a thread that is an instance + * of {@link AbstractYieldingThread}. + *
    + * This annotation can also be used on fields or classes, similar to {@link ThreadRestricted}. + *
    + * In a method annotated with {@link YieldingThreadOnly}, fields and methods annotated with + * {@link YieldingThreadOnly} or {@link AnyThreadSafe} may be used. + *
    + * Methods that are annotated with {@link YieldingThreadOnly} must never call methods that are annotated with + * {@link PotentiallyBlocking}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@Documented +@Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD}) +public @interface YieldingThreadOnly { + + /** + * @see ThreadRestricted#fieldAccess() + */ + Access value() default Access.READ_WRITE; + +} diff --git a/src/main/java/org/galemc/gale/executor/lock/MultipleWaitingBaseThreadsYieldingLock.java b/src/main/java/org/galemc/gale/executor/lock/MultipleWaitingBaseThreadsYieldingLock.java new file mode 100644 index 0000000000000000000000000000000000000000..377e41518a336d4efaf33e3e4257229225761627 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/lock/MultipleWaitingBaseThreadsYieldingLock.java @@ -0,0 +1,42 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.lock; + +import org.galemc.gale.executor.thread.BaseYieldingThread; +import org.galemc.gale.executor.thread.wait.SignalReason; +import org.galemc.gale.executor.thread.wait.WaitingBaseThreadSet; +import org.galemc.gale.executor.thread.wait.WaitingThreadSet; + +import java.util.concurrent.locks.Lock; + +/** + * A {@link YieldingLock} for which multiple {@link BaseYieldingThread}s may be waiting at the same time. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public class MultipleWaitingBaseThreadsYieldingLock extends YieldingLock { + + private final WaitingThreadSet waitingThreads = new WaitingBaseThreadSet(); + + private final SignalReason signalReason = SignalReason.createForWaitingThreadSet(waitingThreads); + + public MultipleWaitingBaseThreadsYieldingLock(Lock innerLock) { + super(innerLock); + } + + @Override + public void addWaitingThread(Thread thread) { + this.waitingThreads.add(thread); + } + + @Override + public void removeWaitingThread(Thread thread) { + this.waitingThreads.remove(thread); + } + + @Override + public SignalReason getSignalReason() { + return this.signalReason; + } + +} 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..1977fb5fb3403a8ecd8e1396bcd1244eb27e78f8 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/lock/YieldingLock.java @@ -0,0 +1,122 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.lock; + +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.PotentiallyYielding; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.thread.AbstractYieldingThread; +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 AbstractYieldingThread}. + * Acquiring it on a thread that is not an {@link AbstractYieldingThread} will perform regular locking + * on the underlying controlled lock, which typically blocks the thread. + * + * @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 an {@link AbstractYieldingThread}, + * 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 + AbstractYieldingThread yieldingThread = AbstractYieldingThread.currentYieldingThread(); + // If we are not on a yielding thread, we wait for the lock instead of yielding + if (yieldingThread == null) { + this.innerLock.lock(); + return; + } + // Otherwise, we yield to other tasks until the lock can be acquired + yieldingThread.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(Thread 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(Thread 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..b38bf6c1da417f00a3c8e2ebb0d2a6df13696921 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/AbstractTaskQueue.java @@ -0,0 +1,104 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.queue; + +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.annotation.thread.BaseYieldingThreadOnly; +import org.galemc.gale.executor.thread.BaseYieldingThread; +import org.galemc.gale.executor.thread.pooled.AbstractYieldingSignallableThreadPool; +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 A name that the queue can be identified by. + */ + String getName(); + + /** + * @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 ScheduledServerThreadTaskQueues} that are scheduled for + * a later tick, while we are already out of spare time this tick. + */ + @AnyThreadSafe + default boolean hasTasksThatCanStartNow(BaseYieldingThread thread) { + return (!thread.isRestrictedDueToYieldDepth && this.hasYieldingTasksThatCanStartNow()) || this.hasFreeTasksThatCanStartNow(); + } + + /** + * @return Whether this queue has any tasks at all. + */ + boolean hasTasks(); + + /** + * Attempts to poll a task. + * + * @param currentThread The current thread. + * @return The polled task, or null if this queue was empty. + */ + @BaseYieldingThreadOnly + @Nullable Runnable poll(BaseYieldingThread 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); + + /** + * Sets the thread pool of this queue. This ensures queues do not accidentally initialize the thread pools + * before they have finished initializing themselves. + */ + void setThreadPool(AbstractYieldingSignallableThreadPool threadPool); + + /** + * @return Whether any of the given task queues is non-empty. + */ + static boolean taskQueuesHaveTasks(AbstractTaskQueue[] queues) { + for (AbstractTaskQueue queue : queues) { + if (queue.hasTasks()) { + return true; + } + } + return false; + } + + /** + * @return Whether any of the given task queues has a task that the given thread could start + * based on amongst others whether {@link BaseYieldingThread#isRestrictedDueToYieldDepth} is true, + * and potentially other external circumstances that may stop a queue from releasing some its held tasks. + */ + static boolean taskQueuesHaveTasksCouldStart(AbstractTaskQueue[] queues, BaseYieldingThread thread) { + for (AbstractTaskQueue queue : queues) { + if (queue.hasTasksThatCanStartNow(thread)) { + return true; + } + } + return false; + } + +} 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..c6672e91706cdeded2f5f43c1b0c4d8dbe2df75e --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledChunkCacheTaskQueue.java @@ -0,0 +1,54 @@ +// Gale - base thread pools + +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.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.thread.pooled.AbstractYieldingSignallableThreadPool; +import org.galemc.gale.executor.thread.pooled.ServerThread; +import org.galemc.gale.executor.thread.pooled.ServerThreadPool; +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 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 server thread tasks, and as + * such we provide access to polling these tasks from a {@link ServerThread}. + *
    + * All tasks provided by this queue must be yield-free. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@AnyThreadSafe +@YieldFree +public final class AllLevelsScheduledChunkCacheTaskQueue extends AllLevelsScheduledTaskQueue { + + AllLevelsScheduledChunkCacheTaskQueue() { + super(); + } + + @Override + public String getName() { + return "AllLevelsScheduledChunkCache"; + } + + @Override + protected boolean hasLevelTasks(ServerLevel level) { + return level.getChunkSource().mainThreadProcessor.hasPendingTasks(); + } + + @Override + protected @Nullable Runnable pollLevel(ServerLevel level) { + var executor = level.getChunkSource().mainThreadProcessor; + if (executor.hasPendingTasks()) { + return executor::pollTask; + } + return null; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledTaskQueue.java new file mode 100644 index 0000000000000000000000000000000000000000..158aa20f2840260306ecd4c48358544384fbf285 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledTaskQueue.java @@ -0,0 +1,103 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.queue; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.thread.BaseYieldingThread; +import org.galemc.gale.executor.thread.pooled.AbstractYieldingSignallableThreadPool; +import org.galemc.gale.executor.thread.wait.SignalReason; +import org.jetbrains.annotations.Nullable; + +/** + * Common implementation for {@link AllLevelsScheduledChunkCacheTaskQueue} and + * {@link AllLevelsScheduledTickThreadChunkTaskQueue}. + *
    + * All tasks provided by this queue must be yield-free. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@AnyThreadSafe +@YieldFree +public abstract class AllLevelsScheduledTaskQueue implements AbstractTaskQueue { + + /** + * Will be initialized in {@link #setThreadPool}. + */ + public SignalReason signalReason; + + /** + * An iteration index for iterating over the levels in {@link #poll}. + */ + private int levelIterationIndex; + + protected AllLevelsScheduledTaskQueue() {} + + protected abstract boolean hasLevelTasks(ServerLevel level); + + protected abstract @Nullable Runnable pollLevel(ServerLevel level); + + @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.isConstructed) { + return false; + } + return this.hasTasks(); + } + + @Override + public boolean hasTasks() { + for (ServerLevel level : MinecraftServer.SERVER.getAllLevels()) { + if (this.hasLevelTasks(level)) { + return true; + } + } + return false; + } + + @Override + public @Nullable Runnable poll(BaseYieldingThread currentThread) { + // Skip during server bootstrap or if there is no more time in the current spare time + if (MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking || !MinecraftServer.isConstructed) { + return null; + } + ServerLevel[] levels = MinecraftServer.SERVER.getAllLevelsArray(); + int startIndex = this.levelIterationIndex = Math.min(this.levelIterationIndex, levels.length - 1); + // Paper - force execution of all worlds, do not just bias the first + do { + ServerLevel level = levels[this.levelIterationIndex++]; + if (this.levelIterationIndex == levels.length) { + this.levelIterationIndex = 0; + } + if (level.serverLevelArrayIndex != -1) { + Runnable task = this.pollLevel(level); + if (task != null) { + return task; + } + } + } while (this.levelIterationIndex != startIndex); + return null; + } + + @Override + public void add(Runnable task, boolean yielding) { + throw new UnsupportedOperationException(); + } + + @Override + public void setThreadPool(AbstractYieldingSignallableThreadPool threadPool) { + if (this.signalReason != null) { + throw new IllegalStateException(this.getClass().getSimpleName() + ".signalReason was already initialized"); + } + this.signalReason = SignalReason.createForThreadPoolNewTasks(threadPool, false); + } + +} diff --git a/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledTickThreadChunkTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledTickThreadChunkTaskQueue.java new file mode 100644 index 0000000000000000000000000000000000000000..dd9f10bbcdfe9b2e5279fb34a913c967d7069fa7 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledTickThreadChunkTaskQueue.java @@ -0,0 +1,53 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.queue; + +import io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler; +import io.papermc.paper.util.TickThread; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.YieldFree; +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 + * from a {@link TickThread}. + *
    + * All tasks provided by this queue must be yield-free. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@AnyThreadSafe +@YieldFree +public final class AllLevelsScheduledTickThreadChunkTaskQueue extends AllLevelsScheduledTaskQueue { + + AllLevelsScheduledTickThreadChunkTaskQueue() { + super(); + } + + @Override + public String getName() { + return "AllLevelsScheduledTickThreadChunk"; + } + + @Override + protected boolean hasLevelTasks(ServerLevel level) { + return level.chunkTaskScheduler.mainThreadExecutor.hasScheduledUncompletedTasksVolatile(); + } + + @Override + protected @Nullable Runnable pollLevel(ServerLevel level) { + var executor = level.chunkTaskScheduler.mainThreadExecutor; + if (executor.hasScheduledUncompletedTasksVolatile()) { + return executor::executeTask; + } + return null; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/queue/AnyTickScheduledServerThreadTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/AnyTickScheduledServerThreadTaskQueue.java new file mode 100644 index 0000000000000000000000000000000000000000..82f3ae8eea503fb51f5c7b0547e71234cc169a07 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/AnyTickScheduledServerThreadTaskQueue.java @@ -0,0 +1,29 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.queue; + +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.YieldFree; + +/** + * 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 AnyTickScheduledServerThreadTaskQueue extends TickRequiredScheduledServerThreadTaskQueue { + + AnyTickScheduledServerThreadTaskQueue() { + super(true); + } + + @Override + public String getName() { + return "AnyTickScheduledServerThread"; + } + +} 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..286b893749db38afc49fd03a0ac006598268d26e --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/BaseTaskQueues.java @@ -0,0 +1,115 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.queue; + +import io.papermc.paper.util.MCUtil; +import io.papermc.paper.util.TickThread; +import org.galemc.gale.executor.thread.pooled.TickAssistThread; +import org.galemc.gale.executor.thread.deferral.TickThreadDeferral; +import org.galemc.gale.executor.thread.AbstractYieldingThread; + +/** + * This class statically provides a list of task queues containing tasks for {@link AbstractYieldingThread}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 a {@link TickThread}, that are procedures that must run + * on a tick 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 #serverThreadTick}. + *
    + * This queue may contain potentially yielding and yield-free tasks. + *
    + * This queue's {@link AbstractTaskQueue#add} must not be called from the server thread, + * because the server thread must not defer to itself (because tasks in this queue are assumed to have to run + * independent of other server 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 deferredToServerThread = new YieldingAndFreeSimpleTaskQueue("DeferredToServerThread", true); + + /** + * This queue stores the tasks scheduled to be executed on a {@link TickThread}, that are procedures that must run + * on a tick 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. + *
    + * This queue may contain potentially yielding and yield-free tasks. + *
    + * This is currently completely unused, because {@link TickThreadDeferral} simply adds task to + * {@link #deferredToServerThread} instead, since there are currently no special {@link TickThread}s. + */ + @SuppressWarnings("unused") + public static final YieldingAndFreeSimpleTaskQueue deferredToUniversalTickThread = new YieldingAndFreeSimpleTaskQueue("DeferredToUniversalTickThread", 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 #anyTickScheduledServerThread} 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 serverThreadTick = new YieldingAndFreeSimpleTaskQueue("ServerThreadTick"); + + /** + * Currently unused: only {@link #anyTickScheduledServerThread} is polled. + * + * @see ThisTickScheduledServerThreadTaskQueue + */ + @SuppressWarnings("unused") + public static final ThisTickScheduledServerThreadTaskQueue thisTickScheduledServerThread = null; + + /** + * @see AnyTickScheduledServerThreadTaskQueue + */ + public static final AnyTickScheduledServerThreadTaskQueue anyTickScheduledServerThread = new AnyTickScheduledServerThreadTaskQueue(); + + /** + * 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 TickAssistThread}), 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 TickAssistThread} 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 tickAssist = new YieldingAndFreeSimpleTaskQueue("TickAssist"); + + /** + * @see AllLevelsScheduledChunkCacheTaskQueue + */ + public static final AllLevelsScheduledChunkCacheTaskQueue allLevelsScheduledChunkCache = new AllLevelsScheduledChunkCacheTaskQueue(); + + /** + * @see AllLevelsScheduledTickThreadChunkTaskQueue + */ + public static final AllLevelsScheduledTickThreadChunkTaskQueue allLevelsScheduledTickThreadChunk = new AllLevelsScheduledTickThreadChunkTaskQueue(); + + /** + * This queue stores the tasks posted to {@link MCUtil#cleanerExecutor}. + */ + public static final FreeSimpleTaskQueue cleaner = new FreeSimpleTaskQueue("Cleaner"); + + /** + * 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("ScheduledAsync"); + +} 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..7bffa7b358c058edbe33bbd2f31438c804c786ef --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/FreeSimpleTaskQueue.java @@ -0,0 +1,53 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.queue; + +import org.galemc.gale.concurrent.UnterminableExecutorService; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +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 + */ +@YieldFree +public class FreeSimpleTaskQueue extends SimpleTaskQueue { + + FreeSimpleTaskQueue(String name) { + super(name, false, true); + } + + FreeSimpleTaskQueue(String name, boolean lifoQueues) { + super(name, 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/ScheduledServerThreadTaskQueues.java b/src/main/java/org/galemc/gale/executor/queue/ScheduledServerThreadTaskQueues.java new file mode 100644 index 0000000000000000000000000000000000000000..12f676a679f4331b39e2c17273f0759a9ce1f5f0 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/ScheduledServerThreadTaskQueues.java @@ -0,0 +1,298 @@ +// Gale - base thread pools + +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.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.Guarded; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.thread.pooled.ServerThread; +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 ScheduledServerThreadTaskQueues { + + ScheduledServerThreadTaskQueues() {} + + 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. + */ + @SuppressWarnings({"rawtypes", "GrazieInspection"}) + @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 SignalReason signalReason; + + /** + * @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. + */ + public static @Nullable Runnable poll(ServerThread currentThread, boolean tryNonCurrentTickQueuesAtAll) { + // Since we assume the tasks in this queue to be potentially yielding, fail if the thread is restricted + if (currentThread.isRestrictedDueToYieldDepth) { + 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; + 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 { + //noinspection NonAtomicOperationOnVolatileField + versionStamp++; + //noinspection unchecked + queues[maxDelay].add(task); + if (maxDelay < firstQueueWithPotentialTasksIndex) { + firstQueueWithPotentialTasksIndex = maxDelay; + } + //noinspection NonAtomicOperationOnVolatileField + 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 { + //noinspection NonAtomicOperationOnVolatileField + 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 + //noinspection unchecked + queues[0].addAll(firstQueue); + firstQueue.clear(); + //noinspection NonAtomicOperationOnVolatileField + 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..7b0de8f921eb8431fa00f1e6a82769165a0efc00 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/SimpleTaskQueue.java @@ -0,0 +1,173 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.queue; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import org.galemc.gale.collection.FIFOConcurrentLinkedQueue; +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.thread.BaseYieldingThread; +import org.galemc.gale.executor.thread.pooled.AbstractYieldingSignallableThreadPool; +import org.galemc.gale.executor.thread.wait.SignalReason; +import org.jetbrains.annotations.Nullable; + +import java.util.Queue; + +/** + * 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 abstract class SimpleTaskQueue implements AbstractTaskQueue { + + /** + * The name of this queue. + * + * @see #getName() + */ + public final String name; + + /** + * 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 final @Nullable Queue yieldingQueue; + + /** + * The queue of yield-free tasks, or null if {@link #canHaveFreeTasks} is false. + */ + private final @Nullable Queue freeQueue; + + /** + * The {@link AbstractYieldingSignallableThreadPool} of which the member threads will poll from this queue. + * Will be initialized in {@link #setThreadPool} by the constructor of the thread pool. + */ + public AbstractYieldingSignallableThreadPool threadPool; + + /** + * The {@link SignalReason} used when signalling threads due to newly added potentially yielding tasks, + * or null if {@link #canHaveYieldingTasks} is false. + * Will be initialized in {@link #setThreadPool}. + */ + private @Nullable SignalReason yieldingSignalReason; + + /** + * The {@link SignalReason} used when signalling threads due to newly added yield-free tasks, + * or null if {@link #canHaveFreeTasks} is false. + * Will be initialized in {@link #setThreadPool}. + */ + private @Nullable SignalReason freeSignalReason; + + SimpleTaskQueue(String name, boolean canHaveYieldingTasks, boolean canHaveFreeTasks) { + this(name, canHaveYieldingTasks, canHaveFreeTasks, false); + } + + /** + * @param name Value for {@link #getName}. + * @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(String name, boolean canHaveYieldingTasks, boolean canHaveFreeTasks, boolean lifoQueues) { + this.name = name; + 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; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public boolean hasYieldingTasksThatCanStartNow() { + //noinspection ConstantConditions + return this.canHaveYieldingTasks && !this.yieldingQueue.isEmpty(); + } + + @Override + public boolean hasFreeTasksThatCanStartNow() { + //noinspection ConstantConditions + return this.canHaveFreeTasks && !this.freeQueue.isEmpty(); + } + + @Override + public boolean hasTasks() { + //noinspection ConstantConditions + return (this.canHaveYieldingTasks && !this.yieldingQueue.isEmpty()) || (this.canHaveFreeTasks && !this.freeQueue.isEmpty()); + } + + @Override + public @Nullable Runnable poll(BaseYieldingThread currentThread) { + Runnable task; + if (!currentThread.isRestrictedDueToYieldDepth && this.canHaveYieldingTasks) { + //noinspection ConstantConditions + 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. + */ + SignalReason lastSignalReason = currentThread.lastSignalReason; + if (lastSignalReason != null && lastSignalReason != this.yieldingSignalReason) { + lastSignalReason.signalAnother(); + } + return task; + } + } + if (this.canHaveFreeTasks) { + //noinspection ConstantConditions + 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. + */ + SignalReason lastSignalReason = currentThread.lastSignalReason; + if (lastSignalReason != null && lastSignalReason != this.freeSignalReason) { + lastSignalReason.signalAnother(); + } + return task; + } + } + return null; + } + + @SuppressWarnings("ConstantConditions") + @Override + public void add(Runnable task, boolean yielding) { + if (yielding) { + this.yieldingQueue.add(task); + this.yieldingSignalReason.signalAnother(); + return; + } + this.freeQueue.add(task); + this.freeSignalReason.signalAnother(); + } + + @Override + public void setThreadPool(AbstractYieldingSignallableThreadPool threadPool) { + if (this.threadPool != null) { + throw new IllegalStateException("SimpleTaskQueue.threadPool was already initialized"); + } + this.threadPool = threadPool; + this.yieldingSignalReason = SignalReason.createForThreadPoolNewTasks(this.threadPool, true); + this.freeSignalReason = SignalReason.createForThreadPoolNewTasks(this.threadPool, false); + } + +} diff --git a/src/main/java/org/galemc/gale/executor/queue/ThisTickScheduledServerThreadTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/ThisTickScheduledServerThreadTaskQueue.java new file mode 100644 index 0000000000000000000000000000000000000000..a6bb58410d0bb741b44cf40a43269c848fdc5b51 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/ThisTickScheduledServerThreadTaskQueue.java @@ -0,0 +1,29 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.queue; + +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.YieldFree; + +/** + * 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 ThisTickScheduledServerThreadTaskQueue extends TickRequiredScheduledServerThreadTaskQueue { + + ThisTickScheduledServerThreadTaskQueue() { + super(false); + } + + @Override + public String getName() { + return "ThisTickScheduledServerThread"; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/queue/TickRequiredScheduledServerThreadTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/TickRequiredScheduledServerThreadTaskQueue.java new file mode 100644 index 0000000000000000000000000000000000000000..421ed98e13bfab6348e5f054e6aedeba89180de6 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/TickRequiredScheduledServerThreadTaskQueue.java @@ -0,0 +1,61 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.queue; + +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.thread.BaseYieldingThread; +import org.galemc.gale.executor.thread.pooled.AbstractYieldingSignallableThreadPool; +import org.galemc.gale.executor.thread.pooled.ServerThread; +import org.galemc.gale.executor.thread.wait.SignalReason; +import org.jetbrains.annotations.Nullable; + +/** + * A common base class for {@link ThisTickScheduledServerThreadTaskQueue} and + * {@link AnyTickScheduledServerThreadTaskQueue}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@AnyThreadSafe +@YieldFree +public abstract class TickRequiredScheduledServerThreadTaskQueue implements AbstractTaskQueue { + + private final boolean tryNonCurrentTickQueuesAtAll; + + protected TickRequiredScheduledServerThreadTaskQueue(boolean tryNonCurrentTickQueuesAtAll) { + this.tryNonCurrentTickQueuesAtAll = tryNonCurrentTickQueuesAtAll; + } + + @Override + public boolean hasYieldingTasksThatCanStartNow() { + return ScheduledServerThreadTaskQueues.hasTasksThatCanStartNow(this.tryNonCurrentTickQueuesAtAll); + } + + @Override + public boolean hasTasks() { + return ScheduledServerThreadTaskQueues.hasTasks(this.tryNonCurrentTickQueuesAtAll); + } + + @Override + public boolean hasFreeTasksThatCanStartNow() { + return false; + } + + @Override + public @Nullable Runnable poll(BaseYieldingThread currentThread) { + return ScheduledServerThreadTaskQueues.poll((ServerThread) currentThread, this.tryNonCurrentTickQueuesAtAll); + } + + @Override + public void add(Runnable task, boolean yielding) { + throw new UnsupportedOperationException(); + } + + @Override + public void setThreadPool(AbstractYieldingSignallableThreadPool threadPool) { + if (ScheduledServerThreadTaskQueues.signalReason == null) { + ScheduledServerThreadTaskQueues.signalReason = SignalReason.createForThreadPoolNewTasks(threadPool, true); + } + } + +} 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..95f8f83abd1a252cd1dbf805f7def97bb2455f9e --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/YieldingAndFreeSimpleTaskQueue.java @@ -0,0 +1,55 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.queue; + +import org.galemc.gale.concurrent.UnterminableExecutorService; +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 + */ +@YieldFree +public class YieldingAndFreeSimpleTaskQueue extends SimpleTaskQueue { + + YieldingAndFreeSimpleTaskQueue(String name) { + super(name, true, true); + } + + YieldingAndFreeSimpleTaskQueue(String name, boolean lifoQueues) { + super(name, 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..015652cb18405fe44b4f8a5fe4b37549998ca795 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/YieldingSimpleTaskQueue.java @@ -0,0 +1,53 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.queue; + +import org.galemc.gale.concurrent.UnterminableExecutorService; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +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 + */ +@YieldFree +public class YieldingSimpleTaskQueue extends SimpleTaskQueue { + + YieldingSimpleTaskQueue(String name) { + super(name, true, false); + } + + YieldingSimpleTaskQueue(String name, boolean lifoQueues) { + super(name, 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/AbstractYieldingThread.java b/src/main/java/org/galemc/gale/executor/thread/AbstractYieldingThread.java new file mode 100644 index 0000000000000000000000000000000000000000..b7c51a6dc54f6879aa662c1431da1d684b348d2a --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/AbstractYieldingThread.java @@ -0,0 +1,69 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.thread; + +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.PotentiallyYielding; +import org.galemc.gale.executor.annotation.thread.ThisThreadOnly; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.lock.YieldingLock; +import org.galemc.gale.executor.thread.pooled.TickAssistThread; +import org.jetbrains.annotations.Nullable; + +import java.util.function.BooleanSupplier; + +/** + * An interface for threads that can yield to other tasks in lieu of blocking. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public interface AbstractYieldingThread extends SignallableThread { + + /** + * Yields to tasks: polls and executes tasks while possible and the stop condition is not met. + * 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 block, waiting 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. + */ + @ThisThreadOnly + @PotentiallyYielding("this method is meant to yield") + void yieldUntil(@Nullable BooleanSupplier stopCondition, @Nullable YieldingLock yieldingLock); + + /** + * 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. + *
    + * The above is the same as {@link #yieldUntil}, except it may be called in situations that is not 'yielding', + * for instance the endless loop polling tasks performed by a {@link TickAssistThread}. The difference with + * {@link #yieldUntil} is that this method does not increment or decrement things like the yield depth of this + * thread, if relevant. + * + * @see #yieldUntil + */ + @ThisThreadOnly + @PotentiallyYielding("may yield further if an executed task is potentially yielding") + void runTasksUntil(@Nullable BooleanSupplier stopCondition, @Nullable YieldingLock yieldingLock); + + /** + * @return The current thread if it is a {@link AbstractYieldingThread}, or null otherwise. + */ + @AnyThreadSafe + @YieldFree + static @Nullable AbstractYieldingThread currentYieldingThread() { + return Thread.currentThread() instanceof AbstractYieldingThread yieldingThread ? yieldingThread : null; + } + + /** + * @return Whether the current thread is a {@link AbstractYieldingThread}. + */ + @AnyThreadSafe + @YieldFree + static boolean isYieldingThread() { + return Thread.currentThread() instanceof AbstractYieldingThread; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/BaseYieldingThread.java b/src/main/java/org/galemc/gale/executor/thread/BaseYieldingThread.java new file mode 100644 index 0000000000000000000000000000000000000000..4502683211202b56bfc0988a74ba63ea8e45e15f --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/BaseYieldingThread.java @@ -0,0 +1,680 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.thread; + +import io.papermc.paper.util.TickThread; +import net.minecraft.server.MinecraftServer; +import org.galemc.gale.executor.annotation.Access; +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.Guarded; +import org.galemc.gale.executor.annotation.PotentiallyBlocking; +import org.galemc.gale.executor.annotation.thread.BaseYieldingThreadOnly; +import org.galemc.gale.executor.annotation.thread.ThisThreadOnly; +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.thread.pooled.ConditionalSurplusThread; +import org.galemc.gale.executor.thread.wait.SignalReason; +import org.galemc.gale.executor.thread.wait.WaitingThreadSet; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.BooleanSupplier; +import java.util.stream.Collectors; + +/** + * An abstract base class implementing {@link AbstractYieldingThread}, + * that provides implementation that is common between + * {@link TickThread} and {@link ConditionalSurplusThread}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public abstract class BaseYieldingThread extends Thread implements AbstractYieldingThread { + + /** + * The minimum time to wait as the {@link MinecraftServer#serverThread} when performing a timed wait. + * Given in nanoseconds. + * If a timed wait with a lower time is attempted, the wait is not performed at all. + */ + public static final long SERVER_THREAD_WAIT_NANOS_MINIMUM = 10_000; + + /** + * The time to wait as the {@link MinecraftServer#serverThread} during the oversleep phase, if + * there may be delayed tasks. + * Given in nanoseconds. + */ + public static final long SERVER_THREAD_WAIT_NANOS_DURING_OVERSLEEP_WITH_DELAYED_TASKS = 50_000; + + /** + * The {@link SignalReason} to pass to {@link #signal(SignalReason)} when this thread becomes allowed + * to poll tasks after having not been allowed to. + */ + public static final SignalReason becomesAllowedToPollSignalReason = SignalReason.createNonRetrying(); + + /** + * The queues that this thread can poll from, in the order they should be attempted to be polled from. + */ + public final AbstractTaskQueue[] taskQueues; + + /** + * The {@link WaitingThreadSet} to add this thread to while waiting for new potentially yielding tasks. + */ + private final WaitingThreadSet threadsWaitingForYieldingTasks; + + /** + * The {@link WaitingThreadSet} to add this thread to while waiting for new yield-free tasks. + */ + private final WaitingThreadSet threadsWaitingForFreeTasks; + + /** + * Whether this thread keeps track of the yield depth in {@link #yieldDepth}. + */ + private final boolean trackYieldDepth; + + /** + * The maximum yield depth. While this thread has a yield depth equal to or greater than this value, + * it can not start more potentially yielding tasks. + *
    + * This value has no effect if {@link #trackYieldDepth} is false. + */ + public final int maximumYieldDepth; + + /** + * The current yield depth of this thread. + *
    + * This value is always 0 if {@link #trackYieldDepth} is false. + */ + @AnyThreadSafe(Access.READ) @ThisThreadOnly(Access.WRITE) + public volatile int yieldDepth = 0; + + /** + * Whether this thread is currently restricted + * due to {@link #yieldDepth} being at least {@link #maximumYieldDepth}. + *
    + * This value is always false if {@link #trackYieldDepth} is false. + */ + @AnyThreadSafe(Access.READ) @ThisThreadOnly(Access.WRITE) + public volatile boolean isRestrictedDueToYieldDepth = false; + + /** + * The lock to guard this thread's sleeping and waking actions. + */ + protected final Lock waitLock = new ReentrantLock(); + + /** + * The condition to wait for a signal, when this thread has to wait for something to do. + */ + protected 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}. + *
    + * This value is used to determine whether to set {@link #skipNextWait} when {@link #signal} is called. + */ + @AnyThreadSafe(Access.READ) @ThisThreadOnly(Access.WRITE) + private volatile boolean isPollingTaskOrCheckingStopCondition = 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) @ThisThreadOnly(Access.WRITE) + @Guarded(value = "#waitLock", fieldAccess = Access.WRITE) + public volatile boolean isWaiting = false; + + /** + * If {@link #isWaiting} is true, whether this thread is registered to be signalled when new tasks are added. + * This value is meaningless while {@link #isWaiting} is false. + */ + public volatile boolean isRegisteredForNewTasks; + + /** + * 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 BaseYieldingThread(Runnable target, String name, AbstractTaskQueue[] taskQueues, WaitingThreadSet threadsWaitingForYieldingTasks, WaitingThreadSet threadsWaitingForFreeTasks, boolean trackYieldDepth, int maximumYieldDepth) { + super(target, name); + this.taskQueues = taskQueues; + this.threadsWaitingForYieldingTasks = threadsWaitingForYieldingTasks; + this.threadsWaitingForFreeTasks = threadsWaitingForFreeTasks; + this.trackYieldDepth = trackYieldDepth; + this.maximumYieldDepth = maximumYieldDepth; + } + + /** + * To be called when this thread wants to poll a task. + * + * @return Whether the polling will be allowed to proceed. If false, the thread will start waiting + * until signalled by {@link #becomesAllowedToPollSignalReason} or another reason that is not a new task being + * added to a queue (for example a {@link YieldingLock} it is trying to acquire being released). + */ + protected abstract boolean wantsToPoll(); + + /** + * To be called when this thread could not poll a task and is going to wait. + * This is not called when the thread goes to sleep due to receiving a {@code false} result from + * {@link #wantsToPoll}. + * + * @return Whether this thread should register as waiting for new tasks to be added. + */ + protected abstract boolean willWaitAfterPollFailure(); + + /** + * To be called when this thread has been signalled by a reason other than + * {@link #becomesAllowedToPollSignalReason}, and waking up from {@link Condition#await}. + */ + protected abstract void wokeUpAfterNonBecomeAllowedToPollSignal(); + + /** + * To be called right before this thread will begin to block. + */ + protected abstract void preBlockThread(); + + /** + * To be called right after this thread blocked. + */ + protected abstract void postBlockThread(); + + @Override + public void yieldUntil(@Nullable BooleanSupplier stopCondition, @Nullable YieldingLock yieldingLock) { + if (this.trackYieldDepth) { + //noinspection NonAtomicOperationOnVolatileField + this.yieldDepth++; + if (!this.isRestrictedDueToYieldDepth && this.yieldDepth >= this.maximumYieldDepth) { + this.isRestrictedDueToYieldDepth = true; + } + } + this.runTasksUntil(stopCondition, yieldingLock); + if (this.trackYieldDepth) { + //noinspection NonAtomicOperationOnVolatileField + this.yieldDepth--; + if (this.isRestrictedDueToYieldDepth && this.yieldDepth < this.maximumYieldDepth) { + this.isRestrictedDueToYieldDepth = false; + } + } + } + + @Override + public void runTasksUntil(@Nullable BooleanSupplier stopCondition, @Nullable YieldingLock yieldingLock) { + this.isPollingTaskOrCheckingStopCondition = true; + + /* + 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. + */ + while (true) { + if (stopCondition != null) { + if (this == MinecraftServer.serverThread) { + MinecraftServer.currentManagedBlockStopConditionHasBecomeTrue = false; + } + if (stopCondition.getAsBoolean()) { + if (this == MinecraftServer.serverThread) { + MinecraftServer.currentManagedBlockStopConditionHasBecomeTrue = true; + } + break; + } + } else { + //noinspection ConstantConditions + if (yieldingLock.tryLock()) { + break; + } + } + + // Check if this thread is allowed to poll + if (!wantsToPoll()) { + // If not, the thread will wait until it is allowed, and then try the loop again + this.waitUntilSignalled(yieldingLock, false, false); + continue; + } + + // If this is the server thread, update isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking + if (this == MinecraftServer.serverThread) { + MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking = MinecraftServer.isInSpareTime && MinecraftServer.blockingCount == 0 && !MinecraftServer.SERVER.haveTime(); + } + + // Attempt to poll a task that can be started + Runnable task = this.pollTask(); + + // Run the task if found + if (task != null) { + + // If this is the server thread, potentially set nextTimeAssumeWeMayHaveDelayedTasks to true + if (this == MinecraftServer.serverThread && !MinecraftServer.nextTimeAssumeWeMayHaveDelayedTasks && AbstractTaskQueue.taskQueuesHaveTasks(this.taskQueues)) { + MinecraftServer.nextTimeAssumeWeMayHaveDelayedTasks = true; + } + + this.isPollingTaskOrCheckingStopCondition = false; + task.run(); + + // If this is the server thread, execute some chunk tasks + if (this == MinecraftServer.serverThread) { + MinecraftServer.SERVER.executeMidTickTasks(); // Paper - execute chunk tasks mid tick + } + + this.isPollingTaskOrCheckingStopCondition = true; + continue; + + } + + /* + If no task that can be started 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.waitUntilSignalled(yieldingLock, true, true); + + } + + this.isPollingTaskOrCheckingStopCondition = false; + + /* + If the thread was signalled for another reason than the lock, but we acquired the lock instead, + another thread should be signalled. + */ + SignalReason lastSignalReason = this.lastSignalReason; + if (lastSignalReason != null && yieldingLock != null && lastSignalReason != yieldingLock.getSignalReason()) { + lastSignalReason.signalAnother(); + } + + } + + /** + * Polls a task from any queue this thread can currently poll from, and returns it. + * Polling potentially yielding tasks is attempted before yield-free tasks. + * + * @return The task that was polled, or null if no task was found. + */ + @ThisThreadOnly + @YieldFree + protected @Nullable Runnable pollTask() { + for (var queue : this.taskQueues) { + Runnable task = queue.poll(this); + if (task != null) { + return task; + } + } + // We failed to poll any task + return null; + } + + /** + * 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 registerForTasks Whether to register to wait for new tasks being added to the task queues. + * @param callWillWait Whether to call {@link #willWaitAfterPollFailure()} + */ + @ThisThreadOnly + @PotentiallyBlocking + private void waitUntilSignalled(@Nullable YieldingLock yieldingLock, boolean registerForTasks, boolean callWillWait) { + + if (callWillWait) { + if (!this.willWaitAfterPollFailure()) { + registerForTasks = false; + } + } + + // Remember whether we registered to wait, to unregister later + boolean registeredAsWaiting = false; + this.isRegisteredForNewTasks = false; + // No point in registering if we're not going to wait anyway + if (!this.skipNextWait) { + // Register this thread with parties that may signal it + this.registerAsWaiting(yieldingLock, registerForTasks); + registeredAsWaiting = true; + } + + /* + If we cannot acquire the lock, we can assume this thread is being signalled, + so there is no reason to start waiting. + */ + waitWithLock: if (this.waitLock.tryLock()) { + try { + + // If it was set that this thread should skip the wait in the meantime, skip it + if (this.skipNextWait) { + break waitWithLock; + } + + // Do a quick last check to not wait if a new task that can be polled was added in the meantime + if (registerForTasks && AbstractTaskQueue.taskQueuesHaveTasksCouldStart(this.taskQueues, this)) { + break waitWithLock; + } + + // Register as waiting + if (!registeredAsWaiting) { + this.registerAsWaiting(yieldingLock, registerForTasks); + registeredAsWaiting = true; + } + + // Mark this thread as waiting + this.isWaiting = true; + + // Wait + try { + + /* + Check if we should wait with a timeout: this only happens if this thread is the server thread, in + which case we do not want to wait past the start of the next tick. + */ + boolean waitedWithTimeout = false; + if (this == MinecraftServer.serverThread) { + // -1 indicates to not use a timeout (this value is not later set to any other negative value) + long waitForNanos = -1; + if (MinecraftServer.isWaitingUntilNextTick) { + /* + During waiting until the next tick, we wait until the next tick start. + If it already passed, we do not have to use a timeout, because we will be notified + when the stop condition becomes true. + */ + waitForNanos = MinecraftServer.nextTickStartNanoTime - System.nanoTime(); + if (waitForNanos < 0) { + waitForNanos = -1; + } + } else if (MinecraftServer.SERVER.isOversleep) { + /* + During this phase, MinecraftServer#mayHaveDelayedTasks() is checked, and we may not + be notified when it changes. Therefore, if the next tick start has not passed, we will + wait until then, but if it has, we wait for a short interval to make sure we keep + checking the stop condition (but not for longer than until the last time we can be + executing extra delayed tasks). + */ + waitForNanos = MinecraftServer.nextTickStartNanoTime - System.nanoTime(); + if (waitForNanos < 0) { + waitForNanos = Math.min(Math.max(0, MinecraftServer.delayedTasksMaxNextTickNanoTime - System.nanoTime()), SERVER_THREAD_WAIT_NANOS_DURING_OVERSLEEP_WITH_DELAYED_TASKS); + } + } + if (waitForNanos >= 0) { + // Set the last signal reason to null in case the timeout elapses without a signal + this.lastSignalReason = null; + // Wait, but at most for the determined time + waitedWithTimeout = true; + // Skip if the time is too short + if (waitForNanos >= SERVER_THREAD_WAIT_NANOS_MINIMUM) { + this.preBlockThread(); + //noinspection ResultOfMethodCallIgnored + this.waitCondition.await(waitForNanos, TimeUnit.NANOSECONDS); + this.postBlockThread(); + } + } + } + + /* + If we did not wait with a timeout, wait indefinitely. If this thread is the server thread, + and the intended start time of the next tick has already passed, but the stop condition to stop + running tasks is still not true, this thread must be signalled when a change in conditions causes + the stop condition to become true. + */ + if (!waitedWithTimeout) { + this.preBlockThread(); + this.waitCondition.await(); + this.postBlockThread(); + } + + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + + // Unmark this thread as waiting + this.isWaiting = false; + + } finally { + this.waitLock.unlock(); + } + } + + if (this.lastSignalReason != becomesAllowedToPollSignalReason) { + this.wokeUpAfterNonBecomeAllowedToPollSignal(); + } + + // Unregister this thread from the parties it was registered with before + if (registeredAsWaiting) { + this.unregisterAsWaiting(yieldingLock); + } + + // Reset skipping the next wait + this.skipNextWait = false; + + } + + /** + * 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. + * @param registerForTasks Whether to register to wait for new tasks being added to the task queues. + */ + @ThisThreadOnly + @YieldFree + private void registerAsWaiting(@Nullable YieldingLock yieldingLock, boolean registerForTasks) { + // Register with the queues that we may poll tasks from + if (registerForTasks && !this.isRegisteredForNewTasks) { + this.registerAsWaitingForTasks(false); + } + // Register with the lock + if (yieldingLock != null) { + yieldingLock.addWaitingThread(this); + } + } + + /** + * Unregisters this thread as waiting for new tasks be added. + *
    + * This must either be called from this thread itself while {@link #isWaiting} is false, + * with {@code mustAcquireLock} given as false, + * or be called from any thread while {@link #isWaiting} is true, + * with {@code mustAcquireLock} given as true. + *
    + * This must only be called when {@link #isRegisteredForNewTasks} is false. + * + * @see #registerAsWaiting + */ + @YieldFree + public void registerAsWaitingForTasks(boolean mustAcquireLock) { + if (mustAcquireLock) { + //noinspection StatementWithEmptyBody + while (!this.waitLock.tryLock()); + } + try { + this.threadsWaitingForYieldingTasks.add(this); + if (!this.isRestrictedDueToYieldDepth) { + this.threadsWaitingForFreeTasks.add(this); + } + this.isRegisteredForNewTasks = true; + } finally { + if (mustAcquireLock) { + this.waitLock.unlock(); + } + } + } + + /** + * Unregisters this thread from the places it was registered with in {@link #registerAsWaiting}. + * + * @see #registerAsWaiting + */ + @ThisThreadOnly + @YieldFree + private void unregisterAsWaiting(@Nullable YieldingLock yieldingLock) { + // Unregister from the task queues + if (this.isRegisteredForNewTasks) { + this.unregisterAsWaitingForTasks(false); + } + // Unregister from the lock + if (yieldingLock != null) { + yieldingLock.removeWaitingThread(this); + } + } + + /** + * Unregisters this thread as waiting for new tasks be added. + *
    + * This must either be called from this thread itself while {@link #isWaiting} is false, + * with {@code mustAcquireLock} given as false, + * or be called from any thread while {@link #isWaiting} is true, + * with {@code mustAcquireLock} given as true. + *
    + * This must only be called when {@link #isRegisteredForNewTasks} is true. + * + * @see #unregisterAsWaiting + */ + @YieldFree + public void unregisterAsWaitingForTasks(boolean mustAcquireLock) { + if (mustAcquireLock) { + //noinspection StatementWithEmptyBody + while (!this.waitLock.tryLock()); + } + try { + this.threadsWaitingForYieldingTasks.remove(this); + if (!this.isRestrictedDueToYieldDepth) { + this.threadsWaitingForFreeTasks.remove(this); + } + this.isRegisteredForNewTasks = false; + } finally { + if (mustAcquireLock) { + this.waitLock.unlock(); + } + } + } + + /** + * 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, + * or whether {@link #skipNextWait} was set to true. + */ + @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.isPollingTaskOrCheckingStopCondition && !this.skipNextWait) { + this.lastSignalReason = reason; + this.skipNextWait = true; + return true; + } + return false; + } finally { + this.waitLock.unlock(); + } + } + + /** + * Signals this thread to wake up. Has no effect if {@link #isWaiting} is false. + * + * @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 signalIfWaiting(@Nullable SignalReason reason) { + //noinspection StatementWithEmptyBody + while (!this.waitLock.tryLock()); + try { + if (this.isWaiting) { + this.lastSignalReason = reason; + this.waitCondition.signal(); + return true; + } + return false; + } finally { + this.waitLock.unlock(); + } + } + + /** + * Allows this thread to poll, either by registering it to listen for tasks if it was not registered, + * or by signalling it if tasks that can be started are already present. + * + * @return Whether this thread was not active before, and is active now. + */ + @AnyThreadSafe + @YieldFree + public boolean activateIfNotListeningForTasksAndNotRestricted() { + //noinspection StatementWithEmptyBody + while (!this.waitLock.tryLock()); + try { + if (this.isWaiting && !this.isRegisteredForNewTasks && !this.isRestrictedDueToYieldDepth) { + if (AbstractTaskQueue.taskQueuesHaveTasks(this.taskQueues)) { + this.lastSignalReason = becomesAllowedToPollSignalReason; + this.waitCondition.signal(); + } else { + this.registerAsWaitingForTasks(false); + } + return true; + } + return false; + } finally { + this.waitLock.unlock(); + } + } + + /** + * Unregisters this thread to listen for tasks if it was not registered. + * + * @return Whether this thread was sleeping and registered to listen for tasks before, + * and was unregistered to listen for tasks. + */ + @AnyThreadSafe + @YieldFree + public boolean deactivateIfListeningForTasks() { + //noinspection StatementWithEmptyBody + while (!this.waitLock.tryLock()); + try { + if (this.isWaiting && this.isRegisteredForNewTasks) { + this.unregisterAsWaitingForTasks(false); + return true; + } + return false; + } finally { + this.waitLock.unlock(); + } + } + /** + * Causes this thread to 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. + */ + @ThisThreadOnly + protected void runForever() { + this.runTasksUntil(() -> false, null); + } + + /** + * A method that simply acquires the {@link BaseYieldingThread} that is the current thread, and calls + * {@link #runForever()} on it. + */ + @BaseYieldingThreadOnly + protected static void getCurrentBaseYieldingThreadAndRunForever() { + ((BaseYieldingThread) Thread.currentThread()).runForever(); + } + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/OriginalServerThread.java b/src/main/java/org/galemc/gale/executor/thread/OriginalServerThread.java new file mode 100644 index 0000000000000000000000000000000000000000..c44607129a79884b1e4f3d9fb8b57cdfa527d267 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/OriginalServerThread.java @@ -0,0 +1,21 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.thread; + +import net.minecraft.server.MinecraftServer; +import org.galemc.gale.executor.thread.pooled.ServerThread; +import org.spigotmc.WatchdogThread; + +/** + * A type that is unique to {@link MinecraftServer#serverThread}, + * to distinguish it from {@link WatchdogThread#instance}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public final class OriginalServerThread extends ServerThread { + + public OriginalServerThread(final Runnable run, final String name) { + super(run, name); + } + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/SignallableThread.java b/src/main/java/org/galemc/gale/executor/thread/SignallableThread.java new file mode 100644 index 0000000000000000000000000000000000000000..0d6cf71da1100c51e3b8aaf8e28765bc82e7eeb5 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/SignallableThread.java @@ -0,0 +1,32 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.thread; + +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.thread.wait.SignalReason; +import org.jetbrains.annotations.Nullable; + +/** + * An interface for threads that can wait (either by blocking or yielding) for events, and be signalled when + * circumstances may have changed. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public interface SignallableThread { + + /** + * 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 had not been signalled to wake up before, + * but has or will be woken up due to this signal. + */ + @AnyThreadSafe + @YieldFree + boolean signal(@Nullable SignalReason reason); + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/deferral/ServerThreadDeferral.java b/src/main/java/org/galemc/gale/executor/thread/deferral/ServerThreadDeferral.java new file mode 100644 index 0000000000000000000000000000000000000000..9d02c2f4360bf19f379b7012206b72abffda886a --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/deferral/ServerThreadDeferral.java @@ -0,0 +1,136 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.thread.deferral; + +import io.papermc.paper.util.TickThread; +import org.galemc.gale.concurrent.UnterminableExecutorService; +import org.galemc.gale.executor.queue.BaseTaskQueues; +import org.galemc.gale.executor.thread.AbstractYieldingThread; +import org.galemc.gale.executor.thread.pooled.ServerThread; +import org.galemc.gale.executor.thread.pooled.TickAssistThread; +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 but not limited to a {@link TickAssistThread}, + * to defer blocks of code to a {@link ServerThread}, and wait for its completion. + *
    + * Using deferral from a {@link TickThread} that is not the correct thread already is highly discouraged + * because yielding from a {@link TickThread} should be avoided whenever possible. + * + * @see TickThreadDeferral + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public final class ServerThreadDeferral { + + private ServerThreadDeferral() {} + + /** + * @see #defer(Supplier, boolean) + */ + public static void defer(Runnable task, boolean yielding) { + deferInternal(task, null, yielding); + } + + /** + * Defers the given {@code task} to a {@link ServerThread}, and yields until it has finished. + * If this thread is a {@link ServerThread}, the task will be executed right away. + *
    + * The task itself must be non-blocking and may be potentially yielding, but keeping the task yield-free is + * highly preferred because during yielding from a {@link ServerThread}, most other tasks that must be + * executed on a {@link ServerThread} cannot be run. + *
    + * On an {@link AbstractYieldingThread}, this method yields until the task is completed. + * Like any potentially yielding method, while technically possible to call from any thread, this method should + * generally only be called from a yielding thread, because on any other thread, the thread will block until + * the given task has been completed by the main thread. + *
    + * If this thread is already an appropriate thread to run the task on, the task is performed on this thread. + * + * @param task The task to run. + * @param yielding Whether the task is potentially yielding. + */ + public static T defer(Supplier task, boolean yielding) { + return deferInternal(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 deferInternal(@Nullable Runnable runnable, @Nullable Supplier supplier, boolean yielding) { + // Check if we are the right thread + if (TickThread.isTickThread()) { + if (runnable == null) { + //noinspection ConstantConditions + return supplier.get(); + } + runnable.run(); + return null; + } + // Otherwise, schedule the task and wait for it to complete + CompletableFuture future = new CompletableFuture<>(); + AbstractYieldingThread yieldingThread = AbstractYieldingThread.currentYieldingThread(); + if (yieldingThread != null) { + // Yield until the task completes + BaseTaskQueues.deferredToServerThread.add(() -> { + if (runnable == null) { + //noinspection ConstantConditions + future.complete(supplier.get()); + } else { + runnable.run(); + future.complete(null); + } + yieldingThread.signal(null); + }, yielding); + yieldingThread.yieldUntil(future::isDone, null); + return future.getNow(null); + } else { + // Block until the task completes + BaseTaskQueues.deferredToServerThread.add(() -> { + if (runnable == null) { + //noinspection ConstantConditions + future.complete(supplier.get()); + } else { + runnable.run(); + future.complete(null); + } + }, 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/deferral/TickThreadDeferral.java b/src/main/java/org/galemc/gale/executor/thread/deferral/TickThreadDeferral.java new file mode 100644 index 0000000000000000000000000000000000000000..74319026715d9a31e2d34fcba251d6d0e828ffe5 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/deferral/TickThreadDeferral.java @@ -0,0 +1,143 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.thread.deferral; + +import io.papermc.paper.util.TickThread; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import org.galemc.gale.concurrent.UnterminableExecutorService; +import org.galemc.gale.executor.thread.AbstractYieldingThread; +import org.galemc.gale.executor.thread.pooled.TickAssistThread; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.function.Supplier; + +/** + * This class provides functionality to allow any thread, + * including but not limited to a {@link TickAssistThread}, + * to defer blocks of code to a {@link TickThread}, and wait for its 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 a thread responsible for the specific aspects of the code + * (thereby avoiding deadlocks caused by the acquisition of multiple locks in various orders, + * and avoiding collisions between parts of code that can not run concurrently, + * which occur especially easy in parts of code that may have to call callbacks of which + * we can only make limited assumptions) and wait for that to finish. + *
    + * This has a number of advantages. + * When we require running code that checks whether it is being run on an appropriate @link TickThread}, + * we can run it this way. Since these parts of code are always performed on a {@link TickThread} + * 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 of the original threads + * (in fact, if the normally guarded blocks of code are always run exclusively to each other + * when deferred this way, we do not need locks at all). + *
    + * When deferring from an {@link AbstractYieldingThread}, + * we yield to other tasks until the deferred block of code has finished. + * When deferring from another type of thread, the thread is blocked. + *
    + * Using deferral from a {@link TickThread} that is not the correct thread already is highly discouraged + * because yielding from a {@link TickThread} should be avoided whenever possible. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public final class TickThreadDeferral { + + private TickThreadDeferral() {} + + /** + * This may be useful in the future. See the documentation of {@link TickThread#taskQueues}. + * + * @see #defer(Runnable, boolean) + */ + public static void defer(final ServerLevel world, final int chunkX, final int chunkZ, Runnable task, boolean yielding) { + defer(task, yielding); + } + + /** + * This may be useful in the future. See the documentation of {@link TickThread#taskQueues}. + * + * @see #defer(Supplier, boolean) + */ + public static T defer(final ServerLevel world, final int chunkX, final int chunkZ, Supplier task, boolean yielding) { + return defer(task, yielding); + } + + /** + * This may be useful in the future. See the documentation of {@link TickThread#taskQueues}. + * + * @see #defer(Runnable, boolean) + */ + public static void defer(final Entity entity, Runnable task, boolean yielding) { + defer(task, yielding); + } + + /** + * This may be useful in the future. See the documentation of {@link TickThread#taskQueues}. + * + * @see #defer(Supplier, boolean) + */ + public static T defer(final Entity entity, Supplier task, boolean yielding) { + return defer(task, yielding); + } + + /** + * @see #defer(Supplier, boolean) + */ + public static void defer(Runnable task, boolean yielding) { + // Current implementation uses ServerThreadDeferral + ServerThreadDeferral.defer(task, yielding); + } + + /** + * Defers the given {@code task} to any {@link TickThread}, and yields until it has finished. + * If this thread is a {@link TickThread}, the task will be executed right away. + *
    + * The task itself must be non-blocking and may be potentially yielding, but keeping the task yield-free is + * highly preferred because during yielding from a {@link TickThread}, other tasks that must be executed on that + * thread cannot be run. + *
    + * On a {@link AbstractYieldingThread}, this method yields until the task is completed. + * Like any potentially yielding method, while technically possible to call from any thread, this method should + * generally only be called from a yielding thread, because on any other thread, the thread will block until + * the given task has been completed by the main thread. + *
    + * If this thread is already an appropriate thread to run the task on, the task is performed on this thread. + * + * @param task The task to run. + * @param yielding Whether the task is potentially yielding. + */ + public static T defer(Supplier task, boolean yielding) { + // Current implementation uses ServerThreadDeferral + return ServerThreadDeferral.defer(task, yielding); + } + + /** + * 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/pooled/AbstractYieldingSignallableThreadPool.java b/src/main/java/org/galemc/gale/executor/thread/pooled/AbstractYieldingSignallableThreadPool.java new file mode 100644 index 0000000000000000000000000000000000000000..b939e9d227650dc2d321cfe3abfaaff3ed1801d6 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/pooled/AbstractYieldingSignallableThreadPool.java @@ -0,0 +1,31 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.thread.pooled; + +import org.galemc.gale.executor.thread.AbstractYieldingThread; +import org.galemc.gale.executor.thread.wait.SignalReason; +import org.galemc.gale.executor.thread.wait.WaitingThreadSet; + +/** + * An interface for thread pools of {@link AbstractYieldingThread}s. + * This thread pool can be signalled when new tasks for its threads are added. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public interface AbstractYieldingSignallableThreadPool { + + /** + * Attempts to signal one thread in this pool that is waiting for new tasks. + * + * @return Whether a thread was signalled (this return value is always accurate). + * + * @see WaitingThreadSet#pollAndSignal + */ + boolean signalNewTasks(SignalReason reason,boolean yielding); + + /** + * @return The number of threads in this pool that are in the {@link Thread.State#RUNNABLE} state. + */ + int getRunnableThreadCount(); + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/pooled/AsyncThread.java b/src/main/java/org/galemc/gale/executor/thread/pooled/AsyncThread.java new file mode 100644 index 0000000000000000000000000000000000000000..7fff5afd87ab9d4aeb0c80d0fced6098569ad1bb --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/pooled/AsyncThread.java @@ -0,0 +1,39 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.thread.pooled; + +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.jetbrains.annotations.Nullable; + +/** + * This class allows using instanceof to quickly check if the current thread is a {@link AsyncThread} + * and provides some thread-local data. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public final class AsyncThread extends ConditionalSurplusThread { + + AsyncThread(AsyncThreadPool pool, int index) { + super(pool, index); + } + + /** + * @return The current thread if it is a {@link AsyncThread}, or null otherwise. + */ + @AnyThreadSafe + @YieldFree + public static @Nullable AsyncThread currentAsyncThread() { + return Thread.currentThread() instanceof AsyncThread asyncThread ? asyncThread : null; + } + + /** + * @return Whether the current thread is a {@link AsyncThread}. + */ + @AnyThreadSafe + @YieldFree + public static boolean isAsyncThread() { + return Thread.currentThread() instanceof AsyncThread; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/pooled/AsyncThreadPool.java b/src/main/java/org/galemc/gale/executor/thread/pooled/AsyncThreadPool.java new file mode 100644 index 0000000000000000000000000000000000000000..b86516e034c920f1c551764cd5cc2cc338ce5535 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/pooled/AsyncThreadPool.java @@ -0,0 +1,66 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.thread.pooled; + +import io.papermc.paper.util.TickThread; +import net.minecraft.server.MinecraftServer; +import org.galemc.gale.executor.queue.AbstractTaskQueue; +import org.galemc.gale.executor.queue.BaseTaskQueues; + +/** + * A {@link ConditionalSurplusThreadPool} with threads that + * can perform tasks that must be executed on an {@link AsyncThread}. + * These tasks could in principle be executed on a {@link ServerThread}, {@link TickThread} or + * {@link TickAssistThread}, but should not be, because these tasks can take a long time and may block the server + * from progressing to the next tick if performed on one of these types of threads. + *
    + * This pool intends to keep {@link #targetParallelism} threads active at any time, + * but subtracts 1 for each active {@link ServerThread}, {@link TickThread} or {@link TickAssistThread}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public final class AsyncThreadPool extends ConditionalSurplusThreadPool { + + public static final String targetParallelismEnvironmentVariable = "gale.threads.async"; + + public static final int ASYNC_THREAD_PRIORITY = Integer.getInteger("gale.thread.priority.async", 6); + + public static final int MAXIMUM_YIELD_DEPTH = Integer.getInteger("gale.maxyielddepth.async", 100); + + /** + * The target number of threads that will be actively in use by this pool. + * Any active threads with higher priority will be dynamically subtracted from this target. + *
    + * This value is always positive. + *
    + * Determined by {@link TargetParallelism#computeTargetParallelism(String)}, which can be overridden using + * the environment variable {@link #targetParallelismEnvironmentVariable}. + */ + public static final int targetParallelism = TargetParallelism.computeTargetParallelism(targetParallelismEnvironmentVariable); + + public static final AsyncThreadPool instance = new AsyncThreadPool(); + + private AsyncThreadPool() { + super(new AsyncThread[0], "Base Async Thread", new AbstractTaskQueue[] { + // The cleaner queue has high priority because it releases resources back to a pool, thereby saving memory + BaseTaskQueues.cleaner, + BaseTaskQueues.scheduledAsync + }, MAXIMUM_YIELD_DEPTH); + } + + @Override + public AsyncThread createThread(int index) { + AsyncThread thread = new AsyncThread(this, index); + thread.setPriority(ASYNC_THREAD_PRIORITY); + return thread; + } + + @Override + protected int computeIntendedActiveThreadCount() { + if (!MinecraftServer.isConstructed) { + return targetParallelism; + } + return targetParallelism - ServerThreadPool.instance.getRunnableThreadCount() - TickAssistThreadPool.instance.getRunnableThreadCount(); + } + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/pooled/ConditionalSurplusThread.java b/src/main/java/org/galemc/gale/executor/thread/pooled/ConditionalSurplusThread.java new file mode 100644 index 0000000000000000000000000000000000000000..16061701d6df1a372d89046fd2065915892b214f --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/pooled/ConditionalSurplusThread.java @@ -0,0 +1,102 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.thread.pooled; + +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.thread.BaseYieldingThread; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A member thread of a {@link ConditionalSurplusThreadPool}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@AnyThreadSafe +@YieldFree +public abstract class ConditionalSurplusThread extends BaseYieldingThread { + + private final ConditionalSurplusThreadPool pool; + + /** + * Prevent race conditions in checks that are done before changing the active thread count, to make sure + * a thread does not check a condition in order to change the active thread count, and the condition is changed + * and the active thread count changed by another thread before the thread can initiate the change itself. + *
    + * This value must always be switched appropriately before the active thread count is changed. + */ + public final AtomicBoolean isCountedInPoolActiveThreadCount = new AtomicBoolean(); + + /** + * The auto-incremented 0-index index this thread has within this pool. + */ + final int index; + + protected ConditionalSurplusThread(ConditionalSurplusThreadPool pool, int index) { + super(BaseYieldingThread::getCurrentBaseYieldingThreadAndRunForever, pool.threadName + " " + index, pool.taskQueues, pool.threadsWaitingForYieldingTasks, pool.threadsWaitingForFreeTasks, true, pool.maximumYieldDepth); + this.pool = pool; + this.index = index; + } + + @Override + protected boolean wantsToPoll() { + // If this thread is an excess thread, just pause here + for (;;) { + // Set isCountedInPoolActiveThreadCount to false in anticipation + if (this.isCountedInPoolActiveThreadCount.getAndSet(false)) { + if (this.pool.decrementActiveThreadCountIfLargerThanIntended()) { + return false; + } + // Undo the earlier isCountedInPoolActiveThreadCount set + if (!this.isCountedInPoolActiveThreadCount.getAndSet(true)) { + return true; + } + // Strange race condition occurrence + continue; + } + // Strange race condition occurrence + return false; + } + } + + @Override + public boolean willWaitAfterPollFailure() { + if (this.isRestrictedDueToYieldDepth) { + if (this.isCountedInPoolActiveThreadCount.getAndSet(false)) { + /* + This thread will not be attempted to be activated by the updateActiveThreads caused below, + because it only activates threads for which isRestrictedDueToYieldDepth is false. + */ + this.pool.decrementActiveThreadCountAndUpdateActiveThreads(); + } + return false; + } + return true; + } + + @Override + public void wokeUpAfterNonBecomeAllowedToPollSignal() { + /* + If the signal was becomesAllowedToPollSignalReason, the activeThreadCount will already have been + incremented in updateActiveThreads, but the signal is different. If this thread woke up and was not + registered for tasks before, that means it went from inactive to active. + */ + if (!this.isRegisteredForNewTasks) { + if (!this.isCountedInPoolActiveThreadCount.getAndSet(true)) { + this.pool.incrementActiveThreadCountAndUpdateActiveThreads(); + } + } + } + + @Override + protected void preBlockThread() { + this.pool.incrementRunnableThreadCount(); + } + + @Override + protected void postBlockThread() { + this.pool.decrementRunnableThreadCount(); + } + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/pooled/ConditionalSurplusThreadPool.java b/src/main/java/org/galemc/gale/executor/thread/pooled/ConditionalSurplusThreadPool.java new file mode 100644 index 0000000000000000000000000000000000000000..22c3ad33a784cb8d6bcdcb8c787029f4922f6123 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/pooled/ConditionalSurplusThreadPool.java @@ -0,0 +1,408 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.thread.pooled; + +import org.galemc.gale.concurrent.Mutex; +import org.galemc.gale.executor.annotation.Access; +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.Guarded; +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.thread.BaseYieldingThread; +import org.galemc.gale.executor.thread.wait.IndexArrayWaitingThreadSet; +import org.galemc.gale.executor.thread.wait.SignalReason; +import org.galemc.gale.executor.thread.wait.WaitingThreadSet; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A pool of threads that can perform specific types of tasks. + * The threads in this thread pool perform an endless loop, polling and executing tasks. + *
    + * This thread pool has a dynamic amount of threads that are intended to be polling or running tasks at a given moment: + * we refer to such threads as active below. If a thread is listening for new tasks to be added, and it is not + * restricted (i.e. it could accept any task any newly spawned thread in this pool could accept) then it counts as + * active too (since it will be signalled as soon as new tasks are added and there would be no use in replacing it + * with a newly spawned thread). + * If the number of active threads becomes lower than this value, surplus threads become allowed, and are signalled + * (and will be spawned and added to the thread pool if not present yet) to start polling tasks, in order to + * reach the intended amount of active threads again. + * If the number of active threads becomes higher than this value, some excess threads will not be able + * to poll new tasks until allowed again. Potentially, The threads will stay in the {@link Thread.State#RUNNABLE} + * state until they finish a task they were already executing. + *
    + * While a thread is not allowed to poll new tasks, it can still be signalled for other reasons, such as a + * {@link YieldingLock} they were waiting for being released. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@AnyThreadSafe +@YieldFree +public abstract class ConditionalSurplusThreadPool implements AbstractYieldingSignallableThreadPool { + + /** + * The name for newly spawned threads. The index of the thread within this pool will be appended. + */ + final String threadName; + + /** + * The value of {@link BaseYieldingThread#taskQueues} that will be passed to member threads. + */ + final AbstractTaskQueue[] taskQueues; + + /** + * A {@link WaitingThreadSet} of member threads waiting for potentially yielding tasks to be added. + */ + final WaitingThreadSet threadsWaitingForYieldingTasks = new WaitingMemberThreadSet(); + + /** + * A {@link WaitingThreadSet} of member threads waiting for yield-free tasks to be added. + */ + final WaitingThreadSet threadsWaitingForFreeTasks = new WaitingMemberThreadSet() { + + @Override + public boolean pollAndSignal(SignalReason reason) { + if (super.pollAndSignal(reason)) { + return true; + } + /* + Waiting for potentially yielding tasks implies waiting for yield-free tasks, + so we signal threads waiting for potentially yielding tasks too. + */ + return threadsWaitingForYieldingTasks.pollAndSignal(reason); + } + + }; + + /** + * The value of {@link BaseYieldingThread#maximumYieldDepth} that will be passed to member threads. + */ + final int maximumYieldDepth; + + /** + * An array of the threads in this pool, indexed by their {@link ConditionalSurplusThread#index}. + */ + @Guarded(value = "#threadsLock", fieldAccess = Access.WRITE) + private volatile T[] threads; + + private final Mutex threadsLock = Mutex.create(); + + /** + * The last computed number of intended active threads. + *
    + * This value is updated in {@link #updateActiveThreads()}, where its value will be based on + * {@link #computeIntendedActiveThreadCount}. + */ + public volatile int lastComputedIntendedActiveThreadCount; + + /** + * The actual number of threads that are active now, i.e. the number of threads that are either runnable and + * polling or executing tasks, or not restricted and awaiting new tasks to be added. + */ + private final AtomicInteger activeThreadCount = new AtomicInteger(); + + /** + * The number of threads that are in the {@link Thread.State#NEW}, {@link Thread.State#RUNNABLE} or + * {@link Thread.State#TERMINATED} states. + */ + private final AtomicInteger runnableThreadCount = new AtomicInteger(); + + /** + * Whether a call to {@link #updateActiveThreads} is ongoing. + */ + private final AtomicBoolean updateActiveThreadsIsOngoing = new AtomicBoolean(); + + /** + * Whether to repeat the outermost loop within {@link #updateActiveThreads} due to a second call to + * {@link #updateActiveThreads} happening while {@link #updateActiveThreadsIsOngoing} was already true. + */ + private volatile boolean repeatUpdateActiveThreadLoop = false; + + protected ConditionalSurplusThreadPool(T[] emptyThreadArray, String threadName, AbstractTaskQueue[] taskQueues, int maximumYieldDepth) { + this.threads = emptyThreadArray; + this.threadName = threadName; + this.taskQueues = taskQueues; + for (AbstractTaskQueue queue : this.taskQueues) { + queue.setThreadPool(this); + } + this.maximumYieldDepth = maximumYieldDepth; + this.intendedActiveThreadCountMayHaveChanged(); + this.updateActiveThreads(); + } + + /** + * @return A newly constructed member thread with the given index within this pool. + */ + public abstract T createThread(int index); + + /** + * @return The number of threads that are intended to be active now. + */ + protected abstract int computeIntendedActiveThreadCount(); + + @Override + public boolean signalNewTasks(SignalReason reason, boolean yielding) { + // First attempt without updating the active threads, then attempt after updating the active threads + for (int attempt = 0; attempt < 2; attempt++) { + if (attempt == 1) { + this.updateActiveThreads(); + } + for (T thread : this.threads) { + if (thread.isWaiting && thread.isRegisteredForNewTasks) { + if (thread.signal(reason)) { + return true; + } + } + } + } + return false; + } + + @Override + public int getRunnableThreadCount() { + return this.runnableThreadCount.get(); + } + + public void incrementRunnableThreadCount() { + this.runnableThreadCount.incrementAndGet(); + } + + public void decrementRunnableThreadCount() { + this.runnableThreadCount.decrementAndGet(); + } + + public T getThreadByIndex(int index) { + return this.threads[index]; + } + + public void decrementActiveThreadCountAndUpdateActiveThreads() { + this.activeThreadCount.decrementAndGet(); + this.updateActiveThreads(); + } + + public void incrementActiveThreadCountAndUpdateActiveThreads() { + this.activeThreadCount.incrementAndGet(); + this.updateActiveThreads(); + } + + /** + * This implementation is based on the implementation of {@link AtomicInteger#updateAndGet}. + * + * @return Whether {@link #activeThreadCount} was updated by this method. + */ + public boolean decrementActiveThreadCountIfLargerThanIntended() { + for (;;) { + int prev = this.activeThreadCount.get(); + if (prev <= this.lastComputedIntendedActiveThreadCount) { + return false; + } + int next = prev - 1; + if (this.activeThreadCount.weakCompareAndSetVolatile(prev, next)) { + return true; + } + } + } + + /** + * This implementation is based on the implementation of {@link AtomicInteger#updateAndGet}. + * + * @return Whether {@link #activeThreadCount} was updated by this method. + */ + public boolean incrementActiveThreadCountIfSmallerThanIntended() { + for (;;) { + int prev = this.activeThreadCount.get(); + if (prev >= this.lastComputedIntendedActiveThreadCount) { + return false; + } + int next = prev + 1; + if (this.activeThreadCount.weakCompareAndSetVolatile(prev, next)) { + return true; + } + } + } + + /** + * Re-computes the number of threads that should be active, and modifies the thread pool accordingly. May add + * new surplus threads or allow or disallow threads to listen for new tasks. + *
    + * Must be called after the value of {@link #computeIntendedActiveThreadCount()} may have changed, and must + * be called after the value of {@link #activeThreadCount} has changed. + */ + void updateActiveThreads() { + if (!updateActiveThreadsIsOngoing.getAndSet(true)) { + try { + // Keep (de-)activating threads while necessary + for (;;) { + int previousActiveThreadCount = this.activeThreadCount.get(); + if (previousActiveThreadCount == this.lastComputedIntendedActiveThreadCount) { + // The number of active threads is perfect + return; + } + // Remember if we made any changes + boolean madeChanges = false; + // There is no point in activating more threads if there are no taskss + boolean previousHadTasksForNewThreads = AbstractTaskQueue.taskQueuesHaveTasks(this.taskQueues); + if (previousActiveThreadCount < this.lastComputedIntendedActiveThreadCount && previousHadTasksForNewThreads) { + // Allow some threads to listen to new tasks, or signal them if there are already tasks available + for (byte alsoTryZeroYieldDepth = 0; alsoTryZeroYieldDepth < 2; alsoTryZeroYieldDepth++) { + for (T thread : this.threads) { + // Speculatively verify that the thread is waiting and not registered for new tasks + if (thread.isWaiting && !thread.isRegisteredForNewTasks) { + // Speculatively check that the thread is non-restricted + if (!thread.isRestrictedDueToYieldDepth) { + // First attempt to choose a thread that already has a positive yield depth + if (alsoTryZeroYieldDepth == 1 || thread.yieldDepth > 0) { + // Continue if the thread is successfully marked as counted active + if (!thread.isCountedInPoolActiveThreadCount.getAndSet(true)) { + boolean mustUndoSetCountedInPoolActiveThreadCount = true; + try { + // Continue if the active thread count is successfully incremented + if (this.incrementActiveThreadCountIfSmallerThanIntended()) { + // Activate the thread if possible + if (!thread.activateIfNotListeningForTasksAndNotRestricted()) { + // If not possible, undo the active thread count increment + if (this.activeThreadCount.decrementAndGet() == this.lastComputedIntendedActiveThreadCount) { + // The number of active threads matches, return + return; + } + } else { + madeChanges = true; + mustUndoSetCountedInPoolActiveThreadCount = false; + if (this.activeThreadCount.get() == this.lastComputedIntendedActiveThreadCount) { + // The number of active threads matches, return + return; + } + } + } + } finally { + if (mustUndoSetCountedInPoolActiveThreadCount) { + // Undo setting isCountedInPoolActiveThreadCount + thread.isCountedInPoolActiveThreadCount.set(false); + } + } + } + } + } + } + } + } + // Add more active threads by adding threads to the thread pool + int surplusThreadsToAdd = this.lastComputedIntendedActiveThreadCount - this.activeThreadCount.get(); + if (surplusThreadsToAdd == 0) { + // The number of active threads matches, return + return; + } else if (surplusThreadsToAdd > 0) { + // We need to add surplus threads + //noinspection StatementWithEmptyBody + while (!this.threadsLock.tryAcquire()) ; + try { + surplusThreadsToAdd = this.lastComputedIntendedActiveThreadCount - this.activeThreadCount.get(); + int oldThreadsLength = this.threads.length; + int newThreadsLength = oldThreadsLength + surplusThreadsToAdd; + T[] newThreads = Arrays.copyOf(this.threads, newThreadsLength); + for (int i = oldThreadsLength; i < newThreadsLength; i++) { + newThreads[i] = createThread(i); + } + madeChanges = true; + this.threads = newThreads; + for (int i = oldThreadsLength; i < newThreadsLength; i++) { + if (!newThreads[i].isCountedInPoolActiveThreadCount.getAndSet(true)) { + this.activeThreadCount.incrementAndGet(); + } + this.incrementRunnableThreadCount(); + newThreads[i].start(); + } + } finally { + this.threadsLock.release(); + } + } + } else { + // Stop some threads from listening to new tasks + for (byte alsoTryPositiveYieldDepth = 0; alsoTryPositiveYieldDepth < 2; alsoTryPositiveYieldDepth++) { + for (T thread : this.threads) { + // Speculatively verify that the thread is waiting and registered for new tasks + if (thread.isWaiting && thread.isRegisteredForNewTasks) { + // First attempt to choose a thread that has a yield depth of zero + if (alsoTryPositiveYieldDepth == 1 || thread.yieldDepth == 0) { + // Continue if the thread is successfully unmarked as counted active + if (thread.isCountedInPoolActiveThreadCount.getAndSet(false)) { + boolean mustUndoSetCountedInPoolActiveThreadCount = true; + try { + // Continue if the active thread count is successfully incremented + if (this.decrementActiveThreadCountIfLargerThanIntended()) { + // Activate the thread if possible + if (!thread.deactivateIfListeningForTasks()) { + // If not possible, undo the active thread count decrement + if (this.activeThreadCount.incrementAndGet() == this.lastComputedIntendedActiveThreadCount) { + // The number of active threads matches, return + return; + } + } else { + madeChanges = true; + mustUndoSetCountedInPoolActiveThreadCount = false; + if (this.activeThreadCount.get() == this.lastComputedIntendedActiveThreadCount) { + // The number of active threads matches, return + return; + } + } + } + } finally { + if (mustUndoSetCountedInPoolActiveThreadCount) { + // Undo setting isCountedInPoolActiveThreadCount + thread.isCountedInPoolActiveThreadCount.set(true); + } + } + } + } + } + } + } + } + // Stop if nothing interesting changed + if (!this.repeatUpdateActiveThreadLoop && !madeChanges && this.activeThreadCount.get() == previousActiveThreadCount && (previousHadTasksForNewThreads || !AbstractTaskQueue.taskQueuesHaveTasks(this.taskQueues))) { + return; + } + this.repeatUpdateActiveThreadLoop = false; + } + } finally { + this.updateActiveThreadsIsOngoing.set(false); + } + } else { + this.repeatUpdateActiveThreadLoop = true; + } + } + + /** + * To be called when the result of {@link #computeIntendedActiveThreadCount()} may have changed. + */ + public void intendedActiveThreadCountMayHaveChanged() { + int previousLastComputedIntendedActiveThreadCount = this.lastComputedIntendedActiveThreadCount; + int newComputedIntendedActiveThreadCount = this.computeIntendedActiveThreadCount(); + if (previousLastComputedIntendedActiveThreadCount != newComputedIntendedActiveThreadCount) { + this.lastComputedIntendedActiveThreadCount = newComputedIntendedActiveThreadCount; + this.updateActiveThreads(); + } + } + + /** + * A {@link WaitingThreadSet} of member threads waiting for tasks to be added. + */ + public class WaitingMemberThreadSet extends IndexArrayWaitingThreadSet { + + @Override + protected int getThreadIndex(T thread) { + return thread.index; + } + + @Override + protected @NotNull T getThreadByIndex(int index) { + return ConditionalSurplusThreadPool.this.threads[index]; + } + + } + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/pooled/ServerThread.java b/src/main/java/org/galemc/gale/executor/thread/pooled/ServerThread.java new file mode 100644 index 0000000000000000000000000000000000000000..048c33d025fcd0b12780c08e53498a10380136e5 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/pooled/ServerThread.java @@ -0,0 +1,77 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.thread.pooled; + +import io.papermc.paper.util.TickThread; +import net.minecraft.server.MinecraftServer; +import org.galemc.gale.executor.thread.BaseYieldingThread; +import org.jetbrains.annotations.NotNull; +import org.spigotmc.WatchdogThread; + +/** + * A {@link TickThread} that provides an implementation for {@link BaseYieldingThread}, + * that is shared between the {@link MinecraftServer#serverThread} and {@link WatchdogThread#instance}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public class ServerThread extends TickThread { + + /** + * Whether this thread is currently blocked, specifically meaning: whether this thread has at some earlier point + * been in the {@link Thread.State#RUNNABLE} state, but is now (or about to be, or just was) in a state that is + * not {@link Thread.State#NEW}, {@link Thread.State#RUNNABLE} or {@link Thread.State#TERMINATED}. + */ + public volatile boolean isBlocked; + + protected ServerThread(final String name) { + super(name); + } + + protected ServerThread(final Runnable run, final String name) { + super(run, name); + } + + @Override + protected boolean wantsToPoll() { + return true; + } + + @Override + protected boolean willWaitAfterPollFailure() { + return true; + } + + @Override + protected void wokeUpAfterNonBecomeAllowedToPollSignal() {} + + @Override + protected void preBlockThread() { + this.isBlocked = true; + TickAssistThreadPool.instance.intendedActiveThreadCountMayHaveChanged(); + AsyncThreadPool.instance.intendedActiveThreadCountMayHaveChanged(); + } + + @Override + protected void postBlockThread() { + this.isBlocked = false; + TickAssistThreadPool.instance.intendedActiveThreadCountMayHaveChanged(); + AsyncThreadPool.instance.intendedActiveThreadCountMayHaveChanged(); + } + + /** + * This method must not be called while {@link MinecraftServer#isConstructed} is false. + * + * @return The global {@link ServerThread} instance, which is either + * {@link MinecraftServer#serverThread}, or {@link WatchdogThread#instance} while the server is shutting + * down and the {@link WatchdogThread} was responsible. + */ + public static @NotNull ServerThread getInstance() { + if (MinecraftServer.SERVER.hasStopped) { + if (MinecraftServer.SERVER.shutdownThread == WatchdogThread.instance) { + return WatchdogThread.instance; + } + } + return MinecraftServer.serverThread; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/pooled/ServerThreadPool.java b/src/main/java/org/galemc/gale/executor/thread/pooled/ServerThreadPool.java new file mode 100644 index 0000000000000000000000000000000000000000..ec0b6055cd653ef4b7f9e86267a563fe857d8942 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/pooled/ServerThreadPool.java @@ -0,0 +1,95 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.thread.pooled; + +import io.papermc.paper.util.TickThread; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import org.galemc.gale.executor.queue.AbstractTaskQueue; +import org.galemc.gale.executor.queue.BaseTaskQueues; +import org.galemc.gale.executor.thread.wait.SignalReason; +import org.galemc.gale.executor.thread.wait.WaitingServerThreadSet; +import org.galemc.gale.executor.thread.wait.WaitingThreadSet; + +/** + * An implementation of {@link AbstractYieldingSignallableThreadPool} for {@link ServerThread}s. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public final class ServerThreadPool implements AbstractYieldingSignallableThreadPool { + + /** + * The queues that {@link ServerThread}s poll from. + *
    + * Some parts of the server can only be safely accessed by one thread at a time. + * If they can not be guarded by a lock (or if this is not desired, + * because if a ticking thread would need to acquire this lock it would block it), + * then these parts of the code are typically deferred to the server thread. + * Based on the current use of the {@link TickThread} class, particularly given the existence of + * {@link TickThread#isTickThreadFor(Entity)} and {@link TickThread#isTickThreadFor(ServerLevel, int, int)}, + * we can deduce that future support for performing some of these actions in parallel is planned. + * In such a case, some server thread tasks may become tasks that must be + * executed on an appropriate {@link TickThread}. + * In that case, the queues below should be changed so that the server thread and any of the + * ticking threads poll from queues that contain tasks appropriate for them. + * For example, {@link BaseTaskQueues#deferredToUniversalTickThread} would be for tasks that can run + * on any ticking thread, and additional queues would need to be added concerning a specific + * subject (like an entity or chunk) with tasks that will be run on whichever ticking thread is the + * ticking thread for that subject at the time of polling. + */ + public static final AbstractTaskQueue[] taskQueues = { + BaseTaskQueues.deferredToServerThread, + BaseTaskQueues.serverThreadTick, + BaseTaskQueues.anyTickScheduledServerThread, + BaseTaskQueues.allLevelsScheduledChunkCache, + BaseTaskQueues.allLevelsScheduledTickThreadChunk + }; + + /** + * A {@link WaitingThreadSet} of {@link ServerThread}s waiting for potentially yielding tasks to be added. + */ + public final static WaitingThreadSet threadsWaitingForYieldingTasks = new WaitingServerThreadSet(); + + /** + * A {@link WaitingThreadSet} of {@link ServerThread}s waiting for yield-free tasks to be added. + */ + public final static WaitingThreadSet threadsWaitingForFreeTasks = new WaitingServerThreadSet() { + + @Override + public boolean pollAndSignal(SignalReason reason) { + if (super.pollAndSignal(reason)) { + return true; + } + /* + Waiting for potentially yielding tasks implies waiting for yield-free tasks, + so we signal threads waiting for potentially yielding tasks too. + */ + return threadsWaitingForYieldingTasks.pollAndSignal(reason); + } + + }; + + public static final ServerThreadPool instance = new ServerThreadPool(); + + private ServerThreadPool() { + for (AbstractTaskQueue queue : taskQueues) { + queue.setThreadPool(this); + } + } + + @Override + public boolean signalNewTasks(SignalReason reason, boolean yielding) { + ServerThread serverThread = ServerThread.getInstance(); + if (!yielding || !serverThread.isRestrictedDueToYieldDepth) { + return serverThread.signal(reason); + } + return false; + } + + @Override + public int getRunnableThreadCount() { + return ServerThread.getInstance().isBlocked ? 0 : 1; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/pooled/TargetParallelism.java b/src/main/java/org/galemc/gale/executor/thread/pooled/TargetParallelism.java new file mode 100644 index 0000000000000000000000000000000000000000..9a4c05bb45b6bef224e42883d7fd6ee7309bf63c --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/pooled/TargetParallelism.java @@ -0,0 +1,75 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.thread.pooled; + +import org.galemc.gale.util.CPUCoresEstimation; + +/** + * A utility class to determine the target parallelism for thread pools by the number of available CPU cores. + *
    + * The value is currently automatically determined according to the following table: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    system corescores spared
    ≤ 30
    [4, 14]1
    [15, 23]2
    [24, 37]3
    [38, 54]4
    [55, 74]5
    [75, 99]6
    [100, 127]7
    [128, 158]8
    [159, 193]9
    [194, 232]10
    [233, 274]11
    ≥ 27512
    + * Then target parallelism = system cores - cores spared. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public final class TargetParallelism { + + private TargetParallelism() {} + + static int computeTargetParallelism(String environmentVariable) { + int parallelismByEnvironmentVariable = Integer.getInteger(environmentVariable, -1); + int targetParallelismBeforeSetAtLeastOne; + if (parallelismByEnvironmentVariable >= 0) { + targetParallelismBeforeSetAtLeastOne = parallelismByEnvironmentVariable; + } else { + int systemCores = CPUCoresEstimation.get(); + int coresSpared; + if (systemCores <= 3) { + coresSpared = 0; + } else if (systemCores <= 14) { + coresSpared = 1; + } else if (systemCores <= 23) { + coresSpared = 2; + } else if (systemCores <= 37) { + coresSpared = 3; + } else if (systemCores <= 54) { + coresSpared = 4; + } else if (systemCores <= 74) { + coresSpared = 5; + } else if (systemCores <= 99) { + coresSpared = 6; + } else if (systemCores <= 127) { + coresSpared = 7; + } else if (systemCores <= 158) { + coresSpared = 8; + } else if (systemCores <= 193) { + coresSpared = 9; + } else if (systemCores <= 232) { + coresSpared = 10; + } else if (systemCores <= 274) { + coresSpared = 11; + } else { + coresSpared = 12; + } + targetParallelismBeforeSetAtLeastOne = systemCores - coresSpared; + } + return Math.max(1, targetParallelismBeforeSetAtLeastOne); + } + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/pooled/TickAssistThread.java b/src/main/java/org/galemc/gale/executor/thread/pooled/TickAssistThread.java new file mode 100644 index 0000000000000000000000000000000000000000..da9265f13dea8a0b971402fa7e09d30f44f38be4 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/pooled/TickAssistThread.java @@ -0,0 +1,39 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.thread.pooled; + +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.YieldFree; +import org.jetbrains.annotations.Nullable; + +/** + * This class allows using instanceof to quickly check if the current thread is a {@link TickAssistThread} + * and provides some thread-local data. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public final class TickAssistThread extends ConditionalSurplusThread { + + TickAssistThread(TickAssistThreadPool pool, int index) { + super(pool, index); + } + + /** + * @return The current thread if it is a {@link TickAssistThread}, or null otherwise. + */ + @AnyThreadSafe + @YieldFree + public static @Nullable TickAssistThread currentTickAssistThread() { + return Thread.currentThread() instanceof TickAssistThread tickAssistThread ? tickAssistThread : null; + } + + /** + * @return Whether the current thread is a {@link TickAssistThread}. + */ + @AnyThreadSafe + @YieldFree + public static boolean isTickAssistThread() { + return Thread.currentThread() instanceof TickAssistThread; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/pooled/TickAssistThreadPool.java b/src/main/java/org/galemc/gale/executor/thread/pooled/TickAssistThreadPool.java new file mode 100644 index 0000000000000000000000000000000000000000..51a94ffbd039260bdb84ee80131ae18280ce6064 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/pooled/TickAssistThreadPool.java @@ -0,0 +1,116 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.thread.pooled; + +import io.papermc.paper.util.TickThread; +import net.minecraft.server.MinecraftServer; +import org.galemc.gale.executor.queue.AbstractTaskQueue; +import org.galemc.gale.executor.queue.BaseTaskQueues; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A {@link ConditionalSurplusThreadPool} with threads that + * can perform tasks that are part of ticking to assist the main ticking thread(s) in doing so. + * Such tasks could in principle be executed on a {@link ServerThread} or {@link TickThread}, but should not be, + * to keep {@link ServerThread}s and {@link TickThread}s immediately available to run tasks specially for them. + *
    + * This pool intends to keep {@link #targetParallelism} threads active at any time, + * but subtracts 1 for each active {@link ServerThread} or {@link TickThread}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public final class TickAssistThreadPool extends ConditionalSurplusThreadPool { + + public static final String targetParallelismEnvironmentVariable = "gale.threads.tickassist"; + + public static final int TICK_ASSIST_THREAD_PRIORITY = Integer.getInteger("gale.thread.priority.tickassist", 7); + + public static final int MAXIMUM_YIELD_DEPTH = Integer.getInteger("gale.maxyielddepth.tickassist", 100); + + /** + * The target number of threads that will be actively in use by this pool. + * Any active threads with higher priority will be dynamically subtracted from this target. + *
    + * This value is always positive. + *
    + * Determined by {@link TargetParallelism#computeTargetParallelism(String)}, which can be overridden using + * the environment variable {@link #targetParallelismEnvironmentVariable}. + */ + public static final int targetParallelism = TargetParallelism.computeTargetParallelism(targetParallelismEnvironmentVariable); + + /** + * This counter makes sure we do not constantly notify {@link AsyncThreadPool} of changes in the + * number of threads in this pool during {@link #updateActiveThreads()}. + */ + public static final AtomicInteger doNotNotifyOfThreadCountChangesCounter = new AtomicInteger(); + + /** + * Whether this pool skipped notifying {@link AsyncThreadPool} due to + * {@link #doNotNotifyOfThreadCountChangesCounter}. + */ + public static volatile boolean skippedNotifyOfThreadCountChanges = false; + + public static final TickAssistThreadPool instance; + static { + // Make sure we do not notify the AsyncThreadPool while instance is not set yet + doNotNotifyOfThreadCountChangesCounter.incrementAndGet(); + instance = new TickAssistThreadPool(); + if (doNotNotifyOfThreadCountChangesCounter.decrementAndGet() == 0 && skippedNotifyOfThreadCountChanges) { + skippedNotifyOfThreadCountChanges = false; + AsyncThreadPool.instance.intendedActiveThreadCountMayHaveChanged(); + } + } + + private TickAssistThreadPool() { + super(new TickAssistThread[0], "Tick Assist Thread", new AbstractTaskQueue[] { + BaseTaskQueues.tickAssist + }, MAXIMUM_YIELD_DEPTH); + } + + @Override + public TickAssistThread createThread(int index) { + TickAssistThread thread = new TickAssistThread(this, index); + thread.setPriority(TICK_ASSIST_THREAD_PRIORITY); + return thread; + } + + @Override + protected int computeIntendedActiveThreadCount() { + if (!MinecraftServer.isConstructed) { + return 0; + } + return targetParallelism - ServerThreadPool.instance.getRunnableThreadCount(); + } + + @Override + public void incrementRunnableThreadCount() { + super.incrementRunnableThreadCount(); + if (doNotNotifyOfThreadCountChangesCounter.get() == 0) { + AsyncThreadPool.instance.intendedActiveThreadCountMayHaveChanged(); + } else { + skippedNotifyOfThreadCountChanges = true; + } + } + + @Override + public void decrementRunnableThreadCount() { + super.decrementRunnableThreadCount(); + if (doNotNotifyOfThreadCountChangesCounter.get() == 0) { + AsyncThreadPool.instance.intendedActiveThreadCountMayHaveChanged(); + } else { + skippedNotifyOfThreadCountChanges = true; + } + } + + @Override + void updateActiveThreads() { + doNotNotifyOfThreadCountChangesCounter.incrementAndGet(); + super.updateActiveThreads(); + if (doNotNotifyOfThreadCountChangesCounter.decrementAndGet() == 0 && skippedNotifyOfThreadCountChanges) { + skippedNotifyOfThreadCountChanges = false; + AsyncThreadPool.instance.intendedActiveThreadCountMayHaveChanged(); + } + } + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/wait/IndexArrayWaitingThreadSet.java b/src/main/java/org/galemc/gale/executor/thread/wait/IndexArrayWaitingThreadSet.java new file mode 100644 index 0000000000000000000000000000000000000000..a3c11208a831a8e55dfd0cfc6698ea571f602e75 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/wait/IndexArrayWaitingThreadSet.java @@ -0,0 +1,161 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.thread.wait; + +import org.galemc.gale.concurrent.Mutex; +import org.galemc.gale.executor.annotation.Access; +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.Guarded; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.lock.YieldingLock; +import org.galemc.gale.executor.thread.SignallableThread; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; + +/** + * A set of waiting threads. 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 abstract class IndexArrayWaitingThreadSet implements WaitingThreadSet { + + /** + * A set of the indices of the threads in this collection. + */ + @Guarded(value = "#lock", fieldAccess = Access.WRITE) + private volatile boolean[] elements = new boolean[0]; + + /** + * An index in {@link #elements} that is likely to contain {@code true}, or an arbitrary but valid index otherwise. + */ + @Guarded(value = "#lock", fieldAccess = Access.WRITE) + private volatile int potentialLowestPresentElementIndex; + + private final Mutex lock = Mutex.create(); + + @Override + public void add(Thread thread) { + @SuppressWarnings("unchecked") + int index = this.getThreadIndex((T) thread); + // Store field in local variable for non-volatile access + boolean[] elements = this.elements; + if (elements.length > index && elements[index]) { + return; + } + //noinspection StatementWithEmptyBody + while (!this.lock.tryAcquire()); + try { + // Store field in local variable for non-volatile access + elements = this.elements; + if (elements.length <= index) { + this.elements = elements = Arrays.copyOf(elements, index + 1); + } + elements[index] = true; + this.potentialLowestPresentElementIndex = index; + } finally { + this.lock.release(); + } + } + + @Override + public void remove(Thread thread) { + @SuppressWarnings("unchecked") + int index = this.getThreadIndex((T) thread); + // Store field in local variable for non-volatile access + boolean[] elements = this.elements; + if (elements.length <= index || !elements[index]) { + return; + } + //noinspection StatementWithEmptyBody + while (!this.lock.tryAcquire()); + try { + this.elements[index] = false; + } finally { + this.lock.release(); + } + } + + @Override + public boolean pollAndSignal(SignalReason reason) { + @Nullable T polled = this.poll(); + //noinspection SimplifiableConditionalExpression + return polled != null ? polled.signal(reason) : false; + } + + private @Nullable T poll() { + if (this.elements.length == 0) { + return null; + } + // Attempt to poll an element while avoiding acquiring the lock for as long as possible + attemptWhileAvoidingLock: while (true) { + // Store field in local variable for non-volatile access + boolean[] elements = this.elements; + int indexToTry = this.potentialLowestPresentElementIndex; + if (!elements[indexToTry]) { + indexToTry = 0; + // On breaking, we found an index with a true value, let's acquire the lock and try + while (!elements[indexToTry]) { + indexToTry++; + if (indexToTry == elements.length) { + // We tried every index, stop attempting to poll an element while avoiding acquiring the lock + break attemptWhileAvoidingLock; + } + } + } + //noinspection StatementWithEmptyBody + while (!this.lock.tryAcquire()); + try { + // Store field in local variable for non-volatile access + elements = this.elements; + if (elements[indexToTry]) { + elements[indexToTry] = false; + return this.getThreadByIndex(indexToTry); + } + } finally { + this.lock.release(); + } + } + /* + Polling an element while avoiding acquiring the lock for as long as possible failed, + we will now quickly try again with the lock to be sure. + */ + //noinspection StatementWithEmptyBody + while (!this.lock.tryAcquire()); + try { + // Store fields in local variables for non-volatile access + boolean[] elements = this.elements; + int potentialLowestPresentElementIndex = this.potentialLowestPresentElementIndex; + if (elements[potentialLowestPresentElementIndex]) { + elements[potentialLowestPresentElementIndex] = false; + return this.getThreadByIndex(potentialLowestPresentElementIndex); + } + for (int index = 0; index < elements.length; index++) { + if (elements[index]) { + elements[index] = false; + return this.getThreadByIndex(index); + } + } + } finally { + this.lock.release(); + } + // Polling an element failed + return null; + } + + /** + * @return The index of the given thread, to be used as input to {@link #getThreadByIndex(int)} later. + */ + protected abstract int getThreadIndex(T thread); + + /** + * @return The thread with the given {@link #getThreadIndex}. This is always non-null + * as this method will never be called with an invalid index. + */ + protected abstract @NotNull T getThreadByIndex(int index); + +} 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..e6189d5b796e369566ed6322a41f57cf4e347981 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/wait/SignalReason.java @@ -0,0 +1,56 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.thread.wait; + +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.thread.SignallableThread; +import org.galemc.gale.executor.thread.pooled.AbstractYieldingSignallableThreadPool; + +/** + * An interface to indicate the reason of a call to {@link SignallableThread#signal}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public interface SignalReason { + + /** + * Signals another {@link SignallableThread} that is waiting for the same reason. + * + * @return Whether any thread was signalled. + */ + boolean signalAnother(); + + @AnyThreadSafe + @YieldFree + static SignalReason createForThreadPoolNewTasks(AbstractYieldingSignallableThreadPool threadPool, boolean yielding) { + return new SignalReason() { + + @Override + public boolean signalAnother() { + return threadPool.signalNewTasks(this, yielding); + } + + }; + } + + @AnyThreadSafe + @YieldFree + static SignalReason createForWaitingThreadSet(WaitingThreadSet waitingThreads) { + return new SignalReason() { + + @Override + public boolean signalAnother() { + return waitingThreads.pollAndSignal(this); + } + + }; + } + + @AnyThreadSafe + @YieldFree + static SignalReason createNonRetrying() { + return () -> false; + } + +} 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..d0cabea27a7cb1f18ed9ffe46409968cbbd8f891 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/wait/WaitingBaseThreadSet.java @@ -0,0 +1,121 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.thread.wait; + +import org.galemc.gale.concurrent.Mutex; +import org.galemc.gale.executor.thread.BaseYieldingThread; +import org.galemc.gale.executor.thread.pooled.AsyncThread; +import org.galemc.gale.executor.thread.pooled.AsyncThreadPool; +import org.galemc.gale.executor.thread.pooled.ServerThread; +import org.galemc.gale.executor.thread.pooled.TickAssistThread; +import org.galemc.gale.executor.thread.pooled.TickAssistThreadPool; +import org.jetbrains.annotations.Nullable; + +/** + * A set of waiting {@link BaseYieldingThread}s that are an instance of either {@link ServerThread}, + * {@link TickAssistThread} or {@link AsyncThread}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public final class WaitingBaseThreadSet implements WaitingThreadSet { + + /** + * The {@link ServerThread}s in this collection, + * or null if no {@link ServerThread} was ever added to this collection. + */ + private @Nullable WaitingServerThreadSet serverThreads; + + /** + * The {@link TickAssistThread}s in this collection, + * or null if no {@link ServerThread} was ever added to this collection. + */ + private @Nullable WaitingThreadSet tickAssistThreads; + + /** + * The {@link AsyncThread}s in this collection, + * or null if no {@link ServerThread} was ever added to this collection. + */ + private @Nullable WaitingThreadSet asyncThreads; + + /** + * A simple lock for initializing the subsets in this collection. + */ + private final Mutex initializeSubsetsLock = Mutex.create(); + + @Override + public void add(Thread thread) { + if (thread instanceof ServerThread) { + if (this.serverThreads == null) { + //noinspection StatementWithEmptyBody + while (!this.initializeSubsetsLock.tryAcquire()); + try { + if (this.serverThreads == null) { + this.serverThreads = new WaitingServerThreadSet(); + } + } finally { + this.initializeSubsetsLock.release(); + } + } + this.serverThreads.add(thread); + } else if (thread instanceof TickAssistThread) { + if (this.tickAssistThreads == null) { + //noinspection StatementWithEmptyBody + while (!this.initializeSubsetsLock.tryAcquire()); + try { + if (this.tickAssistThreads == null) { + this.tickAssistThreads = TickAssistThreadPool.instance.new WaitingMemberThreadSet(); + } + } finally { + this.initializeSubsetsLock.release(); + } + } + this.tickAssistThreads.add(thread); + } else if (thread instanceof AsyncThread) { + if (this.asyncThreads == null) { + //noinspection StatementWithEmptyBody + while (!this.initializeSubsetsLock.tryAcquire()); + try { + if (this.asyncThreads == null) { + this.asyncThreads = AsyncThreadPool.instance.new WaitingMemberThreadSet(); + } + } finally { + this.initializeSubsetsLock.release(); + } + } + this.asyncThreads.add(thread); + } + throw new IllegalArgumentException("WaitingBaseThreadSet.add received a thread that is not a base thread"); + } + + @Override + public void remove(Thread thread) { + if (thread instanceof ServerThread) { + if (this.serverThreads != null) { + this.serverThreads.remove(thread); + } + } else if (thread instanceof TickAssistThread) { + if (this.tickAssistThreads != null) { + this.tickAssistThreads.add(thread); + } + } else if (thread instanceof AsyncThread) { + if (this.asyncThreads != null) { + this.asyncThreads.remove(thread); + } + } + throw new IllegalArgumentException("WaitingBaseThreadSet.remove received a thread that is not a base thread"); + } + + @SuppressWarnings("RedundantIfStatement") + @Override + public boolean pollAndSignal(SignalReason reason) { + if (this.serverThreads != null && this.serverThreads.pollAndSignal(reason)) { + return true; + } else if (this.tickAssistThreads != null && this.tickAssistThreads.pollAndSignal(reason)) { + return true; + } else if (this.asyncThreads != null && this.asyncThreads.pollAndSignal(reason)) { + return true; + } + return false; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/wait/WaitingServerThreadSet.java b/src/main/java/org/galemc/gale/executor/thread/wait/WaitingServerThreadSet.java new file mode 100644 index 0000000000000000000000000000000000000000..2e631d1c3c1cbbad9d22a187077e148b9f60071f --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/wait/WaitingServerThreadSet.java @@ -0,0 +1,37 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.thread.wait; + +import org.galemc.gale.executor.thread.pooled.ServerThread; + +/** + * A {@link WaitingThreadSet} that simply remembers whether a {@link ServerThread} is waiting for tasks. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public class WaitingServerThreadSet implements WaitingThreadSet { + + public volatile boolean waiting; + + @Override + public void add(Thread thread) { + waiting = true; + } + + @Override + public void remove(Thread thread) { + waiting = false; + } + + @Override + public boolean pollAndSignal(SignalReason reason) { + if (waiting) { + //noinspection RedundantIfStatement + if (ServerThread.getInstance().signal(reason)) { + return true; + } + } + return false; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/wait/WaitingThreadSet.java b/src/main/java/org/galemc/gale/executor/thread/wait/WaitingThreadSet.java new file mode 100644 index 0000000000000000000000000000000000000000..a22f4815cbf6411ac32a070d1769c2389c8140b4 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/wait/WaitingThreadSet.java @@ -0,0 +1,52 @@ +// Gale - base thread pools + +package org.galemc.gale.executor.thread.wait; + +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.lock.YieldingLock; +import org.galemc.gale.executor.thread.SignallableThread; + +/** + * An interface for a set of waiting threads. + * 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 interface WaitingThreadSet { + + /** + * Adds a waiting thread. + *
    + * This method must only be called from the given thread itself. + */ + void add(Thread thread); + + /** + * Removes a waiting thread. + *
    + * This method must only be called from the given thread itself. + */ + void remove(Thread thread); + + /** + * Attempts to signal one waiting thread. + *
    + * 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 SignallableThread#signal + */ + boolean pollAndSignal(SignalReason reason); + +} diff --git a/src/main/java/org/spigotmc/SpigotCommand.java b/src/main/java/org/spigotmc/SpigotCommand.java index 3112a8695639c402e9d18710acbc11cff5611e9c..505181f041fc45a8812bf0b5199950022d3b3001 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 pools - 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..8cd1b04dafc16dc96a6ad58fef7930b351c8f147 100644 --- a/src/main/java/org/spigotmc/WatchdogThread.java +++ b/src/main/java/org/spigotmc/WatchdogThread.java @@ -1,6 +1,5 @@ package org.spigotmc; -import java.awt.print.Paper; import java.lang.management.ManagementFactory; import java.lang.management.MonitorInfo; import java.lang.management.ThreadInfo; @@ -8,12 +7,13 @@ import java.util.logging.Level; import java.util.logging.Logger; import net.minecraft.server.MinecraftServer; import org.bukkit.Bukkit; +import org.galemc.gale.executor.thread.pooled.ServerThread; -public final class WatchdogThread extends io.papermc.paper.util.TickThread // Paper - rewrite chunk system +public final class WatchdogThread extends ServerThread // Paper - rewrite chunk system // Gale - base thread pools { public static final boolean DISABLE_WATCHDOG = Boolean.getBoolean("disable.watchdog"); // Paper - private static WatchdogThread instance; + public static WatchdogThread instance; // Gale - base thread pools - 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.serverThread.getId(), Integer.MAX_VALUE ), log ); // Gale - base thread pools log.log( Level.SEVERE, "------------------------------" ); // // Paper start - Only print full dump on long timeouts