From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Martijn Muijsers Date: Sun, 29 Jan 2023 23:41:12 +0100 Subject: [PATCH] Base thread pool License: AGPL-3.0 (https://www.gnu.org/licenses/agpl-3.0.html) Gale - https://galemc.org diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java index cab91880a08c6fdc545804911d295e0f24f4d983..f8bf00ac23f8b86a5dab81668d595d4ad043f09f 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 pool return; } diff --git a/src/main/java/com/mojang/logging/LogUtils.java b/src/main/java/com/mojang/logging/LogUtils.java index 49019b4a9bc4e634d54a9b0acaf9229a5c896f85..6aae3b36bfe3ffc630cd7af250633de3444095e8 100644 --- a/src/main/java/com/mojang/logging/LogUtils.java +++ b/src/main/java/com/mojang/logging/LogUtils.java @@ -6,6 +6,7 @@ import org.apache.logging.log4j.core.LifeCycle; import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.LoggerConfig; import org.apache.logging.log4j.spi.LoggerContext; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.Marker; @@ -66,4 +67,329 @@ public class LogUtils { return LoggerFactory.getLogger(STACK_WALKER.getCallerClass().getSimpleName()); } // Paper end + + // Gale start - base thread pool - thread loggers + public static @NotNull Logger prefixLogger(@NotNull Logger logger, @NotNull Supplier<@NotNull String> prefixSupplier) { + return new org.slf4j.Logger() { + @Override + public String getName() { + return logger.getName(); + } + + @Override + public boolean isTraceEnabled() { + return logger.isTraceEnabled(); + } + + @Override + public void trace(String msg) { + logger.trace(prefixSupplier.get() + msg); + } + + @Override + public void trace(String format, Object arg) { + logger.trace(prefixSupplier.get() + format, arg); + } + + @Override + public void trace(String format, Object arg1, Object arg2) { + logger.trace(prefixSupplier.get() + format, arg1, arg2); + } + + @Override + public void trace(String format, Object... arguments) { + logger.trace(prefixSupplier.get() + format, arguments); + } + + @Override + public void trace(String msg, Throwable t) { + logger.trace(prefixSupplier.get() + msg, t); + } + + @Override + public boolean isTraceEnabled(Marker marker) { + return logger.isTraceEnabled(marker); + } + + @Override + public void trace(Marker marker, String msg) { + logger.trace(marker, prefixSupplier.get() + msg); + } + + @Override + public void trace(Marker marker, String format, Object arg) { + logger.trace(marker, prefixSupplier.get() + format, arg); + } + + @Override + public void trace(Marker marker, String format, Object arg1, Object arg2) { + logger.trace(marker, prefixSupplier.get() + format, arg1, arg2); + } + + @Override + public void trace(Marker marker, String format, Object... argArray) { + logger.trace(marker, prefixSupplier.get() + format, argArray); + } + + @Override + public void trace(Marker marker, String msg, Throwable t) { + logger.trace(marker, prefixSupplier.get() + msg, t); + } + + @Override + public boolean isDebugEnabled() { + return logger.isDebugEnabled(); + } + + @Override + public void debug(String msg) { + logger.debug(prefixSupplier.get() + msg); + } + + @Override + public void debug(String format, Object arg) { + logger.debug(prefixSupplier.get() + format, arg); + } + + @Override + public void debug(String format, Object arg1, Object arg2) { + logger.debug(prefixSupplier.get() + format, arg1, arg2); + } + + @Override + public void debug(String format, Object... arguments) { + logger.debug(prefixSupplier.get() + format, arguments); + } + + @Override + public void debug(String msg, Throwable t) { + logger.debug(prefixSupplier.get() + msg, t); + } + + @Override + public boolean isDebugEnabled(Marker marker) { + return logger.isDebugEnabled(marker); + } + + @Override + public void debug(Marker marker, String msg) { + logger.debug(marker, prefixSupplier.get() + msg); + } + + @Override + public void debug(Marker marker, String format, Object arg) { + logger.debug(marker, prefixSupplier.get() + format, arg); + } + + @Override + public void debug(Marker marker, String format, Object arg1, Object arg2) { + logger.debug(marker, prefixSupplier.get() + format, arg1, arg2); + } + + @Override + public void debug(Marker marker, String format, Object... arguments) { + logger.debug(marker, prefixSupplier.get() + format, arguments); + } + + @Override + public void debug(Marker marker, String msg, Throwable t) { + logger.debug(marker, prefixSupplier.get() + msg, t); + } + + @Override + public boolean isInfoEnabled() { + return logger.isInfoEnabled(); + } + + @Override + public void info(String msg) { + logger.info(prefixSupplier.get() + msg); + } + + @Override + public void info(String format, Object arg) { + logger.info(prefixSupplier.get() + format, arg); + } + + @Override + public void info(String format, Object arg1, Object arg2) { + logger.info(prefixSupplier.get() + format, arg1, arg2); + } + + @Override + public void info(String format, Object... arguments) { + logger.info(prefixSupplier.get() + format, arguments); + } + + @Override + public void info(String msg, Throwable t) { + logger.info(prefixSupplier.get() + msg, t); + } + + @Override + public boolean isInfoEnabled(Marker marker) { + return logger.isInfoEnabled(marker); + } + + @Override + public void info(Marker marker, String msg) { + logger.info(marker, prefixSupplier.get() + msg); + } + + @Override + public void info(Marker marker, String format, Object arg) { + logger.info(marker, prefixSupplier.get() + format, arg); + } + + @Override + public void info(Marker marker, String format, Object arg1, Object arg2) { + logger.info(marker, prefixSupplier.get() + format, arg1, arg2); + } + + @Override + public void info(Marker marker, String format, Object... arguments) { + logger.info(marker, prefixSupplier.get() + format, arguments); + } + + @Override + public void info(Marker marker, String msg, Throwable t) { + logger.info(marker, prefixSupplier.get() + msg, t); + } + + @Override + public boolean isWarnEnabled() { + return logger.isWarnEnabled(); + } + + @Override + public void warn(String msg) { + logger.warn(prefixSupplier.get() + msg); + } + + @Override + public void warn(String format, Object arg) { + logger.warn(prefixSupplier.get() + format, arg); + } + + @Override + public void warn(String format, Object arg1, Object arg2) { + logger.warn(prefixSupplier.get() + format, arg1, arg2); + } + + @Override + public void warn(String format, Object... arguments) { + logger.warn(prefixSupplier.get() + format, arguments); + } + + @Override + public void warn(String msg, Throwable t) { + logger.warn(prefixSupplier.get() + msg, t); + } + + @Override + public boolean isWarnEnabled(Marker marker) { + return logger.isWarnEnabled(marker); + } + + @Override + public void warn(Marker marker, String msg) { + logger.warn(marker, prefixSupplier.get() + msg); + } + + @Override + public void warn(Marker marker, String format, Object arg) { + logger.warn(marker, prefixSupplier.get() + format, arg); + } + + @Override + public void warn(Marker marker, String format, Object arg1, Object arg2) { + logger.warn(marker, prefixSupplier.get() + format, arg1, arg2); + } + + @Override + public void warn(Marker marker, String format, Object... arguments) { + logger.warn(marker, prefixSupplier.get() + format, arguments); + } + + @Override + public void warn(Marker marker, String msg, Throwable t) { + logger.warn(marker, prefixSupplier.get() + msg, t); + } + + @Override + public boolean isErrorEnabled() { + return logger.isErrorEnabled(); + } + + @Override + public void error(String msg) { + logger.error(prefixSupplier.get() + msg); + } + + @Override + public void error(String format, Object arg) { + logger.error(prefixSupplier.get() + format, arg); + } + + @Override + public void error(String format, Object arg1, Object arg2) { + logger.error(prefixSupplier.get() + format, arg1, arg2); + } + + @Override + public void error(String format, Object... arguments) { + logger.error(prefixSupplier.get() + format, arguments); + } + + @Override + public void error(String msg, Throwable t) { + logger.error(prefixSupplier.get() + msg, t); + } + + @Override + public boolean isErrorEnabled(Marker marker) { + return logger.isErrorEnabled(marker); + } + + @Override + public void error(Marker marker, String msg) { + logger.error(marker, prefixSupplier.get() + msg); + } + + @Override + public void error(Marker marker, String format, Object arg) { + logger.error(marker, prefixSupplier.get() + format, arg); + } + + @Override + public void error(Marker marker, String format, Object arg1, Object arg2) { + logger.error(marker, prefixSupplier.get() + format, arg1, arg2); + } + + @Override + public void error(Marker marker, String format, Object... arguments) { + logger.error(marker, prefixSupplier.get() + format, arguments); + } + + @Override + public void error(Marker marker, String msg, Throwable t) { + logger.error(marker, prefixSupplier.get() + msg, t); + } + + }; + } + + public static @NotNull Logger prefixLoggerWithThread(@NotNull Logger logger) { + return prefixLogger(logger, () -> "[" + Thread.currentThread().getName() + "] "); + } + + public static @NotNull Logger getLoggerPrefixedWithThread() { + return prefixLoggerWithThread(LoggerFactory.getLogger(STACK_WALKER.getCallerClass())); + } + + public static @NotNull Logger getClassLoggerPrefixedWithThread() { + return prefixLoggerWithThread(LoggerFactory.getLogger(STACK_WALKER.getCallerClass().getSimpleName())); + } + // Gale end - base thread pool - thread loggers + } diff --git a/src/main/java/io/papermc/paper/configuration/Configurations.java b/src/main/java/io/papermc/paper/configuration/Configurations.java index cf6d50218769e3fecd12dbde70a03b5042feddf4..9d8ee965f7dcd0f416b7aa8368e34b911edef6b0 100644 --- a/src/main/java/io/papermc/paper/configuration/Configurations.java +++ b/src/main/java/io/papermc/paper/configuration/Configurations.java @@ -322,7 +322,7 @@ public abstract class Configurations { YamlConfiguration global = YamlConfiguration.loadConfiguration(this.globalFolder.resolve(this.globalConfigFileName).toFile()); ConfigurationSection worlds = global.createSection(legacyWorldsSectionKey); worlds.set(legacyWorldDefaultsSectionKey, YamlConfiguration.loadConfiguration(this.globalFolder.resolve(this.defaultWorldConfigFileName).toFile())); - for (ServerLevel level : server.getAllLevels()) { + for (ServerLevel level : server.getAllLevelsArray()) { // Gale - base thread pool - optimize server levels worlds.set(level.getWorld().getName(), YamlConfiguration.loadConfiguration(getWorldConfigFile(level).toFile())); } return global; diff --git a/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java b/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java index a82be9c7226348b6c8ed5edfa8dd8262b4f49f07..47a3580caef45ffe71446c247d4e06e332b2fda2 100644 --- a/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java +++ b/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java @@ -289,7 +289,7 @@ public class PaperConfigurations extends Configurations void ensureRunningOnSameThread(Packet packet, T listener, ServerLevel world) throws RunningOnDifferentThreadException { - PacketUtils.ensureRunningOnSameThread(packet, listener, (BlockableEventLoop) world.getServer()); + PacketUtils.ensureRunningOnSameThread(packet, listener, world.getServer()); // Gale - base thread pool } - public static void ensureRunningOnSameThread(Packet packet, T listener, BlockableEventLoop engine) throws RunningOnDifferentThreadException { + public static void ensureRunningOnSameThread(Packet packet, T listener, AbstractBlockableEventLoop engine) throws RunningOnDifferentThreadException { // Gale - base thread pool if (!engine.isSameThread()) { engine.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..40f20806cc06106b4aa8e708467dcea94d23c83e 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,7 @@ 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.BaseTaskQueueTier; import org.slf4j.Logger; // CraftBukkit start @@ -64,7 +60,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 +224,12 @@ public class Main { WorldStem worldstem; + // Gale start - base thread pool + // Initialize the task tiers and queues by calling an arbitrary method on the last tier and queue + //noinspection ResultOfMethodCallIgnored + BaseTaskQueueTier.ASYNC.ordinal(); + // Gale end - base thread pool + 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..f423f6322b6cbb7b73074f84debc8333ad4e64b3 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -40,10 +40,8 @@ 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; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -108,19 +106,8 @@ import net.minecraft.util.ProgressListener; import net.minecraft.util.RandomSource; import net.minecraft.util.SignatureValidator; import net.minecraft.util.datafix.DataFixers; -import net.minecraft.util.profiling.EmptyProfileResults; -import net.minecraft.util.profiling.ProfileResults; -import net.minecraft.util.profiling.ProfilerFiller; -import net.minecraft.util.profiling.ResultField; -import net.minecraft.util.profiling.SingleTickProfiler; import net.minecraft.util.profiling.jfr.JvmProfiler; import net.minecraft.util.profiling.jfr.callback.ProfiledDuration; -import net.minecraft.util.profiling.metrics.profiling.ActiveMetricsRecorder; -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 +148,16 @@ 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.BaseThread; +import org.galemc.gale.executor.thread.OriginalServerThread; +import org.galemc.gale.executor.thread.SignalReason; +import org.galemc.gale.executor.thread.pool.BaseThreadActivation; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; // CraftBukkit start @@ -181,23 +177,26 @@ import net.minecraft.world.level.levelgen.PatrolSpawner; import net.minecraft.world.level.levelgen.PhantomSpawner; import net.minecraft.world.level.levelgen.WorldDimensions; import net.minecraft.world.level.levelgen.presets.WorldPresets; -import org.bukkit.Bukkit; -import org.bukkit.craftbukkit.CraftServer; -import org.bukkit.craftbukkit.Main; -import org.bukkit.craftbukkit.util.CraftChatMessage; -import org.bukkit.craftbukkit.util.LazyPlayerSet; -import org.bukkit.event.player.AsyncPlayerChatPreviewEvent; import org.bukkit.event.server.ServerLoadEvent; // CraftBukkit end import co.aikar.timings.MinecraftTimings; // Paper -public abstract class MinecraftServer extends ReentrantBlockableEventLoop implements CommandSource, AutoCloseable { +public abstract class MinecraftServer extends MinecraftServerBlockableEventLoop implements CommandSource, AutoCloseable { // Gale - base thread pool 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 pool + public static MinecraftServer SERVER; // Paper // Gale - base thread pool - private -> public + + /** + * Whether {@link #SERVER} has been set. + */ + public static boolean isConstructed; + + // Gale end - base thread pool public static final Logger LOGGER = LogUtils.getLogger(); + public static final Optional THREAD_DEBUG_LOGGER = Boolean.FALSE ? Optional.of(LogUtils.prefixLoggerWithThread(LogUtils.prefixLogger(LogUtils.getLogger(), () -> "TEMP DEBUG - "))) : Optional.empty(); // Gale - base thread pool - temporary debug logger public static final String VANILLA_BRAND = "vanilla"; private static final float AVERAGE_TICK_TIME_SMOOTHING = 0.8F; private static final int TICK_STATS_SPAN = 100; @@ -226,6 +225,10 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop registries; private Map, ServerLevel> levels; + // Gale start - base thread pool - optimize server levels + private @NotNull ServerLevel @NotNull [] levelArray = ArrayConstants.emptyServerLevelArray; + private @Nullable ServerLevel overworld; + // Gale end - base thread pool - optimize server levels private PlayerList playerList; private volatile boolean running; private volatile boolean isRestarting = false; // Paper - flag to signify we're attempting to restart @@ -255,10 +258,114 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop static, final -> non-final (but still effectively final) + // Gale start - base thread pool - make fields volatile + private volatile long nextTickTime; + private volatile long delayedTasksMaxNextTickTime; + // Gale end - base thread pool - make fields volatile + + // Gale start - base thread pool + + 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 void signalServerThreadIfCurrentManagedBlockStopConditionBecameTrue() { + if (currentManagedBlockStopConditionHasBecomeTrue) { + // We already signalled the thread + return; + } + var managedBlockStopCondition = currentManagedBlockStopCondition; + if (managedBlockStopCondition == null) { + // There is no ongoing managedBlock cal + return; + } + if (!managedBlockStopCondition.getAsBoolean()) { + // The stop condition is not true + return; + } + currentManagedBlockStopConditionHasBecomeTrue = true; + serverThread.signal(null); + } + + // Gale start - base thread pool + private final PackRepository packRepository; private final ServerScoreboard scoreboard; @Nullable @@ -287,7 +394,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 pool - make fields volatile // CraftBukkit end // Spigot start public static final int TPS = 20; @@ -303,9 +410,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop S spin(Function serverFactory) { + public static S spin(Function serverFactory) { // Gale - base thread pool AtomicReference atomicreference = new AtomicReference(); - Thread thread = new io.papermc.paper.util.TickThread(() -> { // Paper - rewrite chunk system + OriginalServerThread thread = new OriginalServerThread(() -> { // Paper - rewrite chunk system // Gale - base thread pool ((MinecraftServer) atomicreference.get()).runServer(); }, "Server thread"); @@ -324,16 +431,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 pool + serverThread = thread; + BaseThreadActivation.callForUpdate(); + // Gale end - base thread pool this.executor = Util.backgroundExecutor(); } // CraftBukkit start @@ -599,7 +712,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 +1030,10 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop public // Paper start if (this.forceTicks) { return true; @@ -1253,13 +1403,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 pool } private boolean canSleepForTickNoOversleep() { @@ -1268,7 +1418,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { return !this.canSleepForTickNoOversleep(); // Paper - move oversleep into full server tick + // Gale start - base thread pool }); + isInSpareTime = false; + isWaitingUntilNextTick = false; + // Gale end - base thread pool lastTickOversleepTime = (System.nanoTime() - tickOversleepStart) / 1000000L; // Gale - YAPFA - last tick time } - @Override - public TickTask wrapRunnable(Runnable runnable) { - // Paper start - anything that does try to post to main during watchdog crash, run on watchdog - if (this.hasStopped && Thread.currentThread().equals(shutdownThread)) { - runnable.run(); - runnable = () -> {}; - } - // Paper end - return new TickTask(this.tickCount, runnable); - } - - protected boolean shouldRun(TickTask ticktask) { - return ticktask.getTick() + 3 < this.tickCount || this.haveTime(); - } - - @Override - public boolean pollTask() { - boolean flag = this.pollTaskInternal(); - - this.mayHaveDelayedTasks = flag; - return flag; - } - - private boolean pollTaskInternal() { - if (super.pollTask()) { - this.executeMidTickTasks(); // Paper - execute chunk tasks mid tick - return true; - } else { - boolean ret = false; // Paper - force execution of all worlds, do not just bias the first - if (this.haveTime()) { - Iterator iterator = this.getAllLevels().iterator(); - - while (iterator.hasNext()) { - ServerLevel worldserver = (ServerLevel) iterator.next(); - - if (worldserver.getChunkSource().pollTask()) { - ret = true; // Paper - force execution of all worlds, do not just bias the first - } - } - } - - return ret; // Paper - force execution of all worlds, do not just bias the first - } - } - - public void doRunTask(TickTask ticktask) { // CraftBukkit - decompile error - super.doRunTask(ticktask); - } - private void updateStatusIcon(ServerStatus metadata) { Optional optional = Optional.of(this.getFile("server-icon.png")).filter(File::isFile); @@ -1378,14 +1487,19 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { return !this.canOversleep(); + // Gale start - base thread pool }); + isInSpareTime = false; + // Gale end - base thread pool isOversleep = false;MinecraftTimings.serverOversleep.stopTiming(); // Paper end new com.destroystokyo.paper.event.server.ServerTickStartEvent(this.tickCount+1).callEvent(); // Paper ++this.tickCount; + ScheduledServerThreadTaskQueues.shiftTasksForNextTick(); // Gale - base thread pool this.tickChildren(shouldKeepTicking); if (i - this.lastServerStatus >= 5000000000L) { this.lastServerStatus = i; @@ -1420,7 +1534,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0) { this.playerList.saveAll(playerSaveInterval); } - for (ServerLevel level : this.getAllLevels()) { + for (ServerLevel level : this.getAllLevelsArray()) { // Gale - base thread pool - optimize server levels if (level.paperConfig().chunks.autoSaveInterval.value() > 0) { level.saveIncrementally(fullSave); } @@ -1432,7 +1546,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 +1681,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop, ServerLevel> newLevels = Maps.newLinkedHashMap(oldLevels); newLevels.put(level.dimension(), level); this.levels = Collections.unmodifiableMap(newLevels); + // Gale start - base thread pool - optimize server levels + this.levelArray = newLevels.values().toArray(ServerLevel[]::new); + for (int i = 0; i < this.levelArray.length; i++) { + this.levelArray[i].serverLevelArrayIndex = i; + } + this.overworld = null; + // Gale end - base thread pool - optimize server levels } public void removeLevel(ServerLevel level) { @@ -1598,6 +1722,14 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop, ServerLevel> newLevels = Maps.newLinkedHashMap(oldLevels); newLevels.remove(level.dimension()); this.levels = Collections.unmodifiableMap(newLevels); + // Gale start - base thread pool - optimize server levels + level.serverLevelArrayIndex = -1; + this.levelArray = newLevels.values().toArray(ServerLevel[]::new); + for (int i = 0; i < this.levelArray.length; i++) { + this.levelArray[i].serverLevelArrayIndex = i; + } + this.overworld = null; + // Gale end - base thread pool - optimize server levels } // CraftBukkit end @@ -1605,8 +1737,14 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop getAllLevels() { - return this.levels.values(); + return this.levels == null ? Collections.emptyList() : this.levels.values(); // Gale - base thread pool } public String getServerVersion() { @@ -1726,10 +1864,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0) { + return; + } + // Gale end - base thread pool org.spigotmc.AsyncCatcher.catchOp("mid tick chunk task execution"); long startTime = System.nanoTime(); if ((startTime - lastMidTickExecute) <= CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME || (startTime - lastMidTickExecuteFailure) <= TASK_EXECUTION_FAILURE_BACKOFF) { diff --git a/src/main/java/net/minecraft/server/commands/TimeCommand.java b/src/main/java/net/minecraft/server/commands/TimeCommand.java index f0a7a8df3caa2ea765bb0a87cfede71d0995d276..16f3475b059d2b6b85d2b342e84ab32de8e86ac0 100644 --- a/src/main/java/net/minecraft/server/commands/TimeCommand.java +++ b/src/main/java/net/minecraft/server/commands/TimeCommand.java @@ -51,10 +51,11 @@ public class TimeCommand { } public static int setTime(CommandSourceStack source, int time) { - Iterator iterator = io.papermc.paper.configuration.GlobalConfiguration.get().commands.timeCommandAffectsAllWorlds ? source.getServer().getAllLevels().iterator() : com.google.common.collect.Iterators.singletonIterator(source.getLevel()); // CraftBukkit - SPIGOT-6496: Only set the time for the world the command originates in // Paper - add config option for spigot's change + // Gale start - base thread pool - optimize server levels + ServerLevel[] worldservers = io.papermc.paper.configuration.GlobalConfiguration.get().commands.timeCommandAffectsAllWorlds ? source.getServer().getAllLevelsArray() : new ServerLevel[]{source.getLevel()}; // CraftBukkit - SPIGOT-6496: Only set the time for the world the command originates in // Paper - add config option for spigot's change - while (iterator.hasNext()) { - ServerLevel worldserver = (ServerLevel) iterator.next(); + for (ServerLevel worldserver : worldservers) { + // Gale end - base thread pool - optimize server levels // CraftBukkit start TimeSkipEvent event = new TimeSkipEvent(worldserver.getWorld(), TimeSkipEvent.SkipReason.COMMAND, time - worldserver.getDayTime()); @@ -70,10 +71,11 @@ public class TimeCommand { } public static int addTime(CommandSourceStack source, int time) { - Iterator iterator = io.papermc.paper.configuration.GlobalConfiguration.get().commands.timeCommandAffectsAllWorlds ? source.getServer().getAllLevels().iterator() : com.google.common.collect.Iterators.singletonIterator(source.getLevel()); // CraftBukkit - SPIGOT-6496: Only set the time for the world the command originates in // Paper - add config option for spigot's change + // Gale start - base thread pool - optimize server levels + ServerLevel[] worldservers = io.papermc.paper.configuration.GlobalConfiguration.get().commands.timeCommandAffectsAllWorlds ? source.getServer().getAllLevelsArray() : new ServerLevel[]{source.getLevel()}; // CraftBukkit - SPIGOT-6496: Only set the time for the world the command originates in // Paper - add config option for spigot's change - while (iterator.hasNext()) { - ServerLevel worldserver = (ServerLevel) iterator.next(); + for (ServerLevel worldserver : worldservers) { + // Gale end - base thread pool - optimize server levels // CraftBukkit start TimeSkipEvent event = new TimeSkipEvent(worldserver.getWorld(), TimeSkipEvent.SkipReason.COMMAND, time); diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java index 666114daf9e9a3f9f9e0779a3a40dfac09c80d60..1141b3a88e2eb1baa705b4f781353df0305a7c85 100644 --- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java @@ -49,6 +49,7 @@ import net.minecraft.world.level.block.entity.SkullBlockEntity; import net.minecraft.world.level.storage.LevelStorageSource; import org.galemc.gale.command.GaleCommands; import org.galemc.gale.configuration.GaleGlobalConfiguration; +import org.galemc.gale.executor.thread.OriginalServerThread; import org.galemc.gale.util.CPUCoresEstimation; import org.slf4j.Logger; @@ -82,7 +83,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface private final TextFilterClient textFilterClient; // CraftBukkit start - Signature changed - public DedicatedServer(joptsimple.OptionSet options, WorldLoader.DataLoadContext worldLoader, Thread thread, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PackRepository resourcepackrepository, WorldStem worldstem, DedicatedServerSettings dedicatedserversettings, DataFixer datafixer, Services services, ChunkProgressListenerFactory worldloadlistenerfactory) { + public DedicatedServer(joptsimple.OptionSet options, WorldLoader.DataLoadContext worldLoader, OriginalServerThread thread, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PackRepository resourcepackrepository, WorldStem worldstem, DedicatedServerSettings dedicatedserversettings, DataFixer datafixer, Services services, ChunkProgressListenerFactory worldloadlistenerfactory) { // Gale - base thread pool super(options, worldLoader, thread, convertable_conversionsession, resourcepackrepository, worldstem, Proxy.NO_PROXY, datafixer, services, worldloadlistenerfactory); // CraftBukkit end this.settings = dedicatedserversettings; diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java index df4db98618c6c9261b4ec8e2987c4ed26af4bd4b..83a57b9bc59063ed8299f98bc33e14b57f2ea0de 100644 --- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java +++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java @@ -48,6 +48,9 @@ import net.minecraft.world.level.storage.DimensionDataStorage; import net.minecraft.world.level.storage.LevelData; import net.minecraft.world.level.storage.LevelStorageSource; import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; // Paper +import org.galemc.gale.executor.lock.YieldingLock; +import org.galemc.gale.executor.queue.BaseTaskQueues; +import org.galemc.gale.executor.thread.AbstractYieldingThread; public class ServerChunkCache extends ChunkSource { diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java index 37e0b6212fec71ec9662e6be3b1e8bea487eb4a6..e7747b19685fd943d7fbefbfef656f8bb7c359f1 100644 --- a/src/main/java/net/minecraft/server/level/ServerLevel.java +++ b/src/main/java/net/minecraft/server/level/ServerLevel.java @@ -22,6 +22,8 @@ import java.io.Writer; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; @@ -157,6 +159,8 @@ import net.minecraft.world.phys.shapes.BooleanOp; import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; import net.minecraft.world.ticks.LevelTicks; +import org.galemc.gale.executor.annotation.Access; +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; import org.slf4j.Logger; import org.bukkit.Bukkit; import org.bukkit.Location; @@ -189,6 +193,10 @@ public class ServerLevel extends Level implements WorldGenLevel { private static final int MAX_SCHEDULED_TICKS_PER_TICK = 65536; final List players; public final ServerChunkCache chunkSource; + // Gale start - base thread pool + @AnyThreadSafe(Access.READ) + public volatile int serverLevelArrayIndex; + // Gale end - base thread pool 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 pool - optimize server levels { for (Object o : worldData.cache.values() ) { diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java index e000da425ba28330d1edcce3948d2becb39d2bd3..27b6cf330216bc6e41b67940ed6aa848e0251686 100644 --- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java @@ -185,8 +185,9 @@ import net.minecraft.world.phys.shapes.BooleanOp; import net.minecraft.world.phys.shapes.Shapes; 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 +553,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 pool } private CompletableFuture filterTextPacket(T text, BiFunction> filterer) { @@ -891,13 +892,13 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic // 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 pool return; } // Paper start String str = packet.getCommand(); int index = -1; if (str.length() > 64 && ((index = str.indexOf(' ')) == -1 || index >= 64)) { - server.scheduleOnMain(() -> this.disconnect(Component.translatable("disconnect.spam", ArrayConstants.emptyObjectArray), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM)); // Paper - kick event cause // Gale - JettPack - reduce array allocations + 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 pool 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 pool 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 pool } } 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 pool 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 pool return; } } // Paper end // CraftBukkit start if (this.lastBookTick + 20 > MinecraftServer.currentTick) { - server.scheduleOnMain(() -> this.disconnect("Book edited too quickly!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause // Paper - Also ensure this is called on main + 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 pool 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 pool - 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 pool this.disconnect(Component.translatable("multiplayer.disconnect.illegal_characters"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_CHARACTERS); // Paper - add cause - }); // Paper - push to main for event firing + }, ScheduledServerThreadTaskQueues.KICK_FOR_ILLEGAL_CHARACTERS_IN_CHAT_PACKET_TASK_MAX_DELAY); // Paper - push to main for event firing // Gale - base thread pool } 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 pool this.disconnect(Component.translatable("multiplayer.disconnect.illegal_characters"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_CHARACTERS); // Paper - }); // Paper - push to main for event firing + }, ScheduledServerThreadTaskQueues.KICK_FOR_ILLEGAL_CHARACTERS_IN_CHAT_PACKET_TASK_MAX_DELAY); // Paper - push to main for event firing // Gale - base thread pool } 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 pool + this.disconnect(Component.translatable("multiplayer.disconnect.out_of_order_chat"), org.bukkit.event.player.PlayerKickEvent.Cause.OUT_OF_ORDER_CHAT); // Paper - kick event cause + }, ScheduledServerThreadTaskQueues.KICK_FOR_OUT_OF_ORDER_CHAT_PACKET_TASK_MAX_DELAY); // Paper - push to main // Gale - base thread pool 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)); @@ -3296,7 +3294,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 pool return; } } diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java index 6f139e6cbb61bfb2be9b8b886bec7cddbb2c8993..0cbef825129b173a5244a195ea68444c216c0b1b 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 pool - 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 pool - 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 pool + 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 pool + } + ); + } + + 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 pool - this patch was removed from Paper but might be useful later MutableComponent ichatmutablecomponent; if (player.getGameProfile().getName().equalsIgnoreCase(s)) { @@ -1523,10 +1574,8 @@ public abstract class PlayerList { public void setViewDistance(int viewDistance) { this.viewDistance = viewDistance; //this.broadcastAll(new ClientboundSetChunkCacheRadiusPacket(viewDistance)); // Paper - move into setViewDistance - Iterator iterator = this.server.getAllLevels().iterator(); - while (iterator.hasNext()) { - ServerLevel worldserver = (ServerLevel) iterator.next(); + for (ServerLevel worldserver : this.server.getAllLevelsArray()) { // Gale - base thread pool - optimize server levels if (worldserver != null) { worldserver.getChunkSource().setViewDistance(viewDistance); @@ -1538,10 +1587,8 @@ public abstract class PlayerList { public void setSimulationDistance(int simulationDistance) { this.simulationDistance = simulationDistance; //this.broadcastAll(new ClientboundSetSimulationDistancePacket(simulationDistance)); // Paper - handled by playerchunkloader - Iterator iterator = this.server.getAllLevels().iterator(); - while (iterator.hasNext()) { - ServerLevel worldserver = (ServerLevel) iterator.next(); + for (ServerLevel worldserver : this.server.getAllLevelsArray()) { // Gale - base thread pool - optimize server levels if (worldserver != null) { worldserver.getChunkSource().setSimulationDistance(simulationDistance); diff --git a/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java b/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java index 83701fbfaa56a232593ee8f11a3afb8941238bfa..392e7b4a89669f16b32043b65b69e6593d17f10e 100644 --- a/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java +++ b/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java @@ -6,17 +6,18 @@ import com.mojang.logging.LogUtils; import java.util.List; import java.util.Queue; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; import java.util.concurrent.locks.LockSupport; import java.util.function.BooleanSupplier; import java.util.function.Supplier; + import net.minecraft.util.profiling.metrics.MetricCategory; import net.minecraft.util.profiling.metrics.MetricSampler; import net.minecraft.util.profiling.metrics.MetricsRegistry; import net.minecraft.util.profiling.metrics.ProfilerMeasured; +import org.galemc.gale.executor.AbstractBlockableEventLoop; import org.slf4j.Logger; -public abstract class BlockableEventLoop implements ProfilerMeasured, ProcessorHandle, Executor { +public abstract class BlockableEventLoop implements ProfilerMeasured, ProcessorHandle, AbstractBlockableEventLoop { // Gale - base thread pool private final String name; private static final Logger LOGGER = LogUtils.getLogger(); private final Queue pendingRunnables = Queues.newConcurrentLinkedQueue(); @@ -31,6 +32,7 @@ public abstract class BlockableEventLoop implements Profiler protected abstract boolean shouldRun(R task); + @Override // Gale - base thread pool public boolean isSameThread() { return Thread.currentThread() == this.getRunningThread(); } @@ -45,6 +47,12 @@ public abstract class BlockableEventLoop implements Profiler return this.pendingRunnables.size(); } + // Gale start - base thread pool + public boolean hasPendingTasks() { + return !this.pendingRunnables.isEmpty(); + } + // Gale end - base thread pool + @Override public String name() { return this.name; @@ -102,6 +110,7 @@ public abstract class BlockableEventLoop implements Profiler } + @Override // Gale - base thread pool public void executeIfPossible(Runnable runnable) { this.execute(runnable); } diff --git a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java index 93f722537a6b479561d79ba2e980c50a324f9486..728f52999ea980ea2cef97ba80d3da96dffac7d8 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 pool - optimize server levels if (level == this.level) continue; final Entity entity = level.getEntity(this.ownerUUID); if (entity != null) { diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java index e23fdd5ba09b50b7eef0ca4f36c5480779fba624..a7bb3275b2da8308696b18fb527514f9c4859d35 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 pool - optimize server levels // world.serverLevelData.setDifficulty(config.difficulty); // Paper - per level difficulty world.setSpawnSettings(world.serverLevelData.getDifficulty() != Difficulty.PEACEFUL && config.spawnMonsters, config.spawnAnimals); // Paper - per level difficulty (from MinecraftServer#setDifficulty(ServerLevel, Difficulty, boolean)) @@ -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 pool - optimize server levels //Preconditions.checkState(!this.console.isIteratingOverLevels, "Cannot create a world while worlds are being ticked"); // Paper - Cat - Temp disable. We'll see how this goes. Validate.notNull(creator, "Creator may not be null"); @@ -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 pool - optimize server levels net.minecraft.world.entity.Entity entity = world.getEntity(uuid); if (entity != null) { return entity.getBukkitEntity(); diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java index 54932d92b13b890b07f827c5f09bd137383d4ab5..9d7da4fcf4ab450b6f3d53a07c299884fe29cda0 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; @@ -2372,11 +2370,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 pool net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk)c; if (chunk != null) addTicket(x, z); // Paper ret.complete(chunk == null ? null : chunk.getBukkitChunk()); - }); + }, ScheduledServerThreadTaskQueues.COMPLETE_CHUNK_FUTURE_TASK_MAX_DELAY); // Gale - base thread pool }); return ret; diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java index 78f53ee557276de85f0431ebcb146445b1f4fb92..6176867eea06c53882dcaacfbde0334b39b903cc 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 pool 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 pool }); return ret; diff --git a/src/main/java/org/galemc/gale/configuration/GaleConfigurations.java b/src/main/java/org/galemc/gale/configuration/GaleConfigurations.java index 9571aae593999d11b3908856b0295a7d6b588007..ed2841d3a6c6d90ad02266f38c0821bca4f549f1 100644 --- a/src/main/java/org/galemc/gale/configuration/GaleConfigurations.java +++ b/src/main/java/org/galemc/gale/configuration/GaleConfigurations.java @@ -264,7 +264,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 pool + } 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..e9d778a078bee6b6f1c21078c445b48fc276e985 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/AbstractBlockableEventLoop.java @@ -0,0 +1,21 @@ +// Gale - base thread pool + +package org.galemc.gale.executor; + +import net.minecraft.util.thread.BlockableEventLoop; + +import java.util.concurrent.Executor; + +/** + * An interface for the common functionality of {@link BlockableEventLoop} and {@link MinecraftServerBlockableEventLoop}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public interface AbstractBlockableEventLoop extends Executor { + + boolean isSameThread(); + + @SuppressWarnings("unused") + 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..2da7a1a787bed2e03039796d56201870548ad0e4 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/MinecraftServerBlockableEventLoop.java @@ -0,0 +1,187 @@ +// Gale - base thread pool + +package org.galemc.gale.executor; + +import com.mojang.logging.LogUtils; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.thread.BlockableEventLoop; +import net.minecraft.util.thread.ProcessorHandle; +import net.minecraft.util.thread.ReentrantBlockableEventLoop; +import org.galemc.gale.executor.annotation.thread.ServerThreadOnly; +import org.galemc.gale.executor.queue.BaseTaskQueues; +import org.galemc.gale.executor.queue.ScheduledServerThreadTaskQueues; +import org.galemc.gale.executor.thread.ServerThread; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; + +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(null, 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/TaskSpan.java b/src/main/java/org/galemc/gale/executor/TaskSpan.java new file mode 100644 index 0000000000000000000000000000000000000000..99dcb582bf6557c54d9e1477434710ba92b2d87a --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/TaskSpan.java @@ -0,0 +1,70 @@ +// Gale - base thread pool + +package org.galemc.gale.executor; + +import java.util.Arrays; + +/** + * An enum for the behaviour of a task, in terms of its potential to yield + * and its expected time cost to finish. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public enum TaskSpan { + + /** + * Indicates that a task that may potentially yield. + * The expected duration of this task is not specifically established. + */ + YIELDING(true), + /** + * Indicates that a task is yield-free. + * The expected duration of this task is not specifically established. + */ + FREE(false), + /** + * Indicates that a task is yield-free, + * and has an expected running time that is below double the approximate time cost of two context switches. + * It is assumed that a context switch takes approximately 5 microseconds, and as such, + * this span indicates that a task has an expected running time of 20 or fewer microseconds. + */ + TINY(false); + + /** + * Equal to {@link #ordinal()}. + */ + public final int ordinal; + + /** + * Whether tasks with this span are potentially yielding. + */ + public final boolean isYielding; + + /** + * Equal to the negation of {@link #isYielding}. + */ + public final boolean isNotYielding; + + TaskSpan(boolean isYielding) { + this.ordinal = this.ordinal(); + this.isYielding = isYielding; + this.isNotYielding = !this.isYielding; + } + + /** + * Equal to {@link #values()}. + */ + public static final TaskSpan[] VALUES = values(); + + /** + * Equal to {@link #VALUES}{@code .length}. + */ + public static final int length = VALUES.length; + + /** + * Equal to {@link #VALUES} for which {@link #isNotYielding} is true. + */ + public static final TaskSpan[] NON_YIELDING_VALUES = Arrays.stream(VALUES).filter(span -> span.isNotYielding).toList().toArray(new TaskSpan[length - 1]); + + +} 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 a4dc0ebe48fdd352387f06be42ff46fc11ee5822..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,6 +2,8 @@ package org.galemc.gale.executor.annotation; +import org.galemc.gale.executor.thread.AbstractYieldingThread; + import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Target; @@ -15,6 +17,9 @@ import java.lang.annotation.Target; *
    * In a method annotated with {@link PotentiallyBlocking}, fields and methods annotated with * {@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 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 44b70d68ba6823ab72ea9af4b7774051785c0a2b..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,13 +2,16 @@ 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; /** * An annotation primarily for methods, identifying methods that do not block, but may yield to other tasks - * under certain circumstances. + * under certain circumstances, such as when attempting to acquire a {@link YieldingLock}. *
    * When applied to a class, this annotation indicates it holds for all methods, both instance and static, * belonging to the class, or any superclass thereof, or any inner or statically nested class of the class or @@ -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/AssistThreadOnly.java b/src/main/java/org/galemc/gale/executor/annotation/thread/AssistThreadOnly.java new file mode 100644 index 0000000000000000000000000000000000000000..203799c5a9ddec3e665a7476f5e48a2c0f457b04 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/annotation/thread/AssistThreadOnly.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.AssistThread; + +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 AssistThread}. + *
    + * This annotation can also be used on fields or classes, similar to {@link ThreadRestricted}. + *
    + * In a method annotated with {@link AssistThreadOnly}, fields and methods annotated with + * {@link AssistThreadOnly}, {@link BaseThreadOnly} or {@link AnyThreadSafe} may be used. + *
    + * Methods that are annotated with {@link AssistThreadOnly} must never call methods that are annotated with + * {@link PotentiallyBlocking}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@SuppressWarnings("unused") +@Documented +@Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD}) +public @interface AssistThreadOnly { + + /** + * @see ThreadRestricted#fieldAccess() + */ + Access value() default Access.READ_WRITE; + +} diff --git a/src/main/java/org/galemc/gale/executor/annotation/thread/BaseThreadOnly.java b/src/main/java/org/galemc/gale/executor/annotation/thread/BaseThreadOnly.java new file mode 100644 index 0000000000000000000000000000000000000000..e682953181dbf208e731ab5b081664a129210310 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/annotation/thread/BaseThreadOnly.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.BaseThread; + +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 BaseThread}. + *
    + * This annotation can also be used on fields or classes, similar to {@link ThreadRestricted}. + *
    + * In a method annotated with {@link BaseThreadOnly}, fields and methods annotated with + * {@link BaseThreadOnly} or {@link AnyThreadSafe} may be used. + *
    + * Methods that are annotated with {@link BaseThreadOnly} 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 BaseThreadOnly { + + /** + * @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..d6476e007de11fb4e556cee6ec6eea107dc814fa --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/annotation/thread/OriginalServerThreadOnly.java @@ -0,0 +1,39 @@ +// 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}. + *
    + * In a method annotated with {@link OriginalServerThreadOnly}, fields and methods annotated with + * {@link OriginalServerThreadOnly}, {@link ServerThreadOnly}, {@link BaseThreadOnly} + * or {@link AnyThreadSafe} may be used. + *
    + * 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..d27ee27a65635d0136c5c9e33925b64036ae5cd3 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/annotation/thread/ServerThreadOnly.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.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 BaseThreadOnly} 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/lock/MultipleWaitingBaseThreadsYieldingLock.java b/src/main/java/org/galemc/gale/executor/lock/MultipleWaitingBaseThreadsYieldingLock.java new file mode 100644 index 0000000000000000000000000000000000000000..a248a9ea644a8bb4175da2e1903483ab6866bc48 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/lock/MultipleWaitingBaseThreadsYieldingLock.java @@ -0,0 +1,39 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.lock; + +import org.galemc.gale.executor.thread.BaseThread; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; + +/** + * A {@link YieldingLock} for which multiple {@link BaseThread}s may be waiting at the same time. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@SuppressWarnings("unused") +public class MultipleWaitingBaseThreadsYieldingLock extends YieldingLock { + + private final AtomicInteger waitingThreadCount = new AtomicInteger(); + + public MultipleWaitingBaseThreadsYieldingLock(Lock innerLock) { + super(innerLock); + } + + @Override + public void incrementWaitingThreads() { + this.waitingThreadCount.incrementAndGet(); + } + + @Override + public void decrementWaitingThreads() { + this.waitingThreadCount.decrementAndGet(); + } + + @Override + protected boolean hasWaitingThreads() { + return this.waitingThreadCount.get() > 0; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/lock/SingleWaitingBaseThreadYieldingLock.java b/src/main/java/org/galemc/gale/executor/lock/SingleWaitingBaseThreadYieldingLock.java new file mode 100644 index 0000000000000000000000000000000000000000..686e16da8372085196d8f92adb881f82dd5c2947 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/lock/SingleWaitingBaseThreadYieldingLock.java @@ -0,0 +1,39 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.lock; + +import org.galemc.gale.executor.thread.BaseThread; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; + +/** + * A {@link YieldingLock} for which one {@link BaseThread} may be waiting at any time. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@SuppressWarnings("unused") +public class SingleWaitingBaseThreadYieldingLock extends YieldingLock { + + private volatile boolean hasWaitingThread = false; + + public SingleWaitingBaseThreadYieldingLock(Lock innerLock) { + super(innerLock); + } + + @Override + public void incrementWaitingThreads() { + hasWaitingThread = true; + } + + @Override + public void decrementWaitingThreads() { + hasWaitingThread = false; + } + + @Override + protected boolean hasWaitingThreads() { + return this.hasWaitingThread; + } + +} 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..44b8bd5fd9a3ee2e484c81104523ba956f8d982f --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/lock/YieldingLock.java @@ -0,0 +1,213 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.lock; + +import org.galemc.gale.concurrent.CheckableLock; +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.BaseThread; +import org.galemc.gale.executor.thread.pool.BaseThreadActivation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * 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. + *
    + * A thread cannot acquire a {@link YieldingLock} when it already owns one. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@AnyThreadSafe +public abstract class YieldingLock implements CheckableLock { + + private final Lock innerLock; + + /** + * The same value as {@link #lock}, or null if it is not an instance of {@link CheckableLock}. + */ + private final @Nullable CheckableLock innerCheckableLock; + + /** + * The same value as {@link #lock}, or null if it is not an instance of {@link ReentrantLock}. + */ + private final @Nullable ReentrantLock innerReentrantLock; + + /** + * Whether the {@link #innerLock} can be truly 'held' as lock. + * Can be set to false via {@link #withCanNotBeHeld()} for locks that are not real, but + * merely use the {@link Lock} interface allow base threads to wait for them. + */ + private boolean canBeHeld; + + /** + * Whether any threads can be {@linkplain BaseThread#signal signalled} for this lock being unlocked. + * This is set to false when a thread is signalled, until the thread has actually attempted to + * acquire the lock, so that we do not activate multiple threads for the same unlocking event. + */ + public volatile boolean canBeSignalledFor = true; + + public YieldingLock(Lock innerLock) { + this.innerLock = innerLock; + if (innerLock instanceof CheckableLock checkableLock) { + this.innerCheckableLock = checkableLock; + this.innerReentrantLock = null; + } else if (innerLock instanceof ReentrantLock reentrantLock) { + this.innerCheckableLock = null; + this.innerReentrantLock = reentrantLock; + } else { + throw new IllegalArgumentException("The innerLock passed to the YieldingLock() constructor must be an instance of CheckableLock or ReentrantLock"); + } + } + + /** + * Attempts to acquire the lock immediately. + * + * @return Whether the lock was acquired. + */ + @YieldFree + @Override + public boolean tryLock() { + if (innerLock.tryLock()) { + // Increment the YieldingLock count of the current thread + if (this.canBeHeld) { + @Nullable AbstractYieldingThread yieldingThread = AbstractYieldingThread.currentYieldingThread(); + if (yieldingThread != null) { + yieldingThread.incrementHeldYieldingLockCount(); + } + } + return true; + } + return false; + } + + /** + * 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() { + // Find out our current yielding thread, if any + @Nullable AbstractYieldingThread yieldingThread = AbstractYieldingThread.currentYieldingThread(); + // Try to acquire the lock straight away + if (!this.innerLock.tryLock()) { + // 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, null, this); + } + // Increment the YieldingLock count of the current thread + if (this.canBeHeld) { + if (yieldingThread != null) { + yieldingThread.incrementHeldYieldingLockCount(); + } + } + } + + /** + * Releases the lock (must be called after having completed the computation block that required the lock). + */ + @Override + public void unlock() { + this.innerLock.unlock(); + // Decrement the YieldingLock count of the current thread + if (this.canBeHeld) { + @Nullable AbstractYieldingThread yieldingThread = AbstractYieldingThread.currentYieldingThread(); + if (yieldingThread != null) { + yieldingThread.decrementHeldYieldingLockCount(); + } + } + /* + Potentially signal a thread that this lock has become available. + 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). + Also note that this lock may still be locked (since it may be reentrant) so we check the locked state. + */ + if (!this.isLocked() && this.hasWaitingThreads()) { + BaseThreadActivation.yieldingLockWithWaitingThreadsWasUnlocked(); + } + } + + @SuppressWarnings("RedundantThrows") + @Override + public boolean tryLock(long l, @NotNull TimeUnit timeUnit) throws InterruptedException { + throw new UnsupportedOperationException(); + } + + @SuppressWarnings("RedundantThrows") + @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(); + } + + @Override + public boolean isLocked() { + //noinspection DataFlowIssue + return this.innerCheckableLock != null ? this.innerCheckableLock.isLocked() : this.innerReentrantLock.isLocked(); + } + + @Override + public boolean isHeldByCurrentThread() { + return this.innerCheckableLock != null ? this.innerCheckableLock.isHeldByCurrentThread() : this.innerReentrantLock.isHeldByCurrentThread(); + } + + /** + * Increments the number of threads waiting for this lock to be released. + */ + @YieldFree + public abstract void incrementWaitingThreads(); + + /** + * Decrements the number of threads waiting for this lock to be released. + */ + @YieldFree + public abstract void decrementWaitingThreads(); + + /** + * @return Whether this lock has any threads waiting for it. + */ + @YieldFree + protected abstract boolean hasWaitingThreads(); + + /** + * Sets {@link #canBeHeld} to false. + * This should be called immediately after construction of this {@link YieldingLock}. + * + * @return This instance. + */ + @YieldFree + public @NotNull YieldingLock withCanNotBeHeld() { + this.canBeHeld = false; + return this; + } + +} 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..633a9b7998b304057d1780b2a4a1f0bc7160c6f6 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/AbstractTaskQueue.java @@ -0,0 +1,102 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.queue; + +import org.galemc.gale.executor.TaskSpan; +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.annotation.thread.BaseThreadOnly; +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.ServerThread; +import org.jetbrains.annotations.Nullable; + +/** + * An interface for a task queue that may contain tasks of specific {@link TaskSpan}s. + *
    + * All tasks must be non-blocking. + * + * @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 can ever be polled from while yielding. + *
    + * Some queues should not be yielded to, because they contain tasks that may take excessively long. + */ + default boolean canBeYieldedTo() { + return true; + } + + /** + * @return Whether this queue has any tasks at all. + */ + boolean hasTasks(); + + /** + * @return Whether this queue has any tasks with the given {@link TaskSpan}. + */ + boolean hasTasks(TaskSpan span); + + /** + * @return Whether this queue supports tasks of the given {@link TaskSpan} at all. + */ + boolean canHaveTasks(TaskSpan span); + + /** + * Attempts to poll a task. + *
    + * This must not be called on queues in the {@link BaseTaskQueueTier#SERVER} tier by threads that are not an + * instance of {@link ServerThread}. + *
    + * This must not be called on queues apart from the {@link BaseTaskQueueTier#SERVER} tier by threads that are an + * instance of {@link ServerThread}. Use {@link #pollTiny(BaseThread)} instead. + * + * @param currentThread The current thread. + * @return The polled task, or null if this queue was empty. + */ + @BaseThreadOnly + @Nullable Runnable poll(BaseThread currentThread); + + /** + * Attempts to poll a {@link TaskSpan#TINY} task. + * + * @see #poll(BaseThread) + */ + @BaseThreadOnly + @Nullable Runnable pollTiny(BaseThread currentThread); + + /** + * Schedules a new task to this queue. + * + * @param task The task to schedule. + * @param span The {@link TaskSpan} of the task. + */ + void add(Runnable task, TaskSpan span); + + /** + * Sets the tier of this queue. This ensures queues do not accidentally initialize the tiers + * before the tiers have finished initializing themselves. + */ + void setTier(BaseTaskQueueTier tier); + + /** + * @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; + } + +} 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..b4172f285fbed1f314891b2f729aa2dc27b9ab9b --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledTaskQueue.java @@ -0,0 +1,133 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.queue; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import org.galemc.gale.executor.TaskSpan; +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.pool.BaseThreadActivation; +import org.jetbrains.annotations.Nullable; + +/** + * Common implementation for queues with scheduled tasks for all levels. + *
    + * 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 { + + /** + * The {@link TaskSpan} of tasks in this queue. The span must be yield-free. + */ + public final TaskSpan span; + + /** + * Value of {@code onlyIfLastTimeIsTooLongAgo} in calls to + * {@link BaseThreadActivation#newTaskWasAdded(BaseTaskQueueTier, TaskSpan, boolean)}. + */ + private final boolean onlyNotifyBaseThreadPoolOfNewTasksIfLastTimeIsTooLongAgo; + + /** + * Will be initialized in {@link #setTier}. + */ + private BaseTaskQueueTier tier; + + /** + * An iteration index for iterating over the levels in {@link #poll}. + */ + private int levelIterationIndex; + + protected AllLevelsScheduledTaskQueue(TaskSpan span, boolean onlyNotifyBaseThreadPoolOfNewTasksIfLastTimeIsTooLongAgo) { + this.span = span; + this.onlyNotifyBaseThreadPoolOfNewTasksIfLastTimeIsTooLongAgo = onlyNotifyBaseThreadPoolOfNewTasksIfLastTimeIsTooLongAgo; + } + + protected abstract boolean hasLevelTasks(ServerLevel level); + + protected abstract @Nullable Runnable pollLevel(ServerLevel level); + + @Override + public boolean hasTasks() { + if (MinecraftServer.SERVER == null) { + return false; + } + for (ServerLevel level : MinecraftServer.SERVER.getAllLevels()) { + if (this.hasLevelTasks(level)) { + return true; + } + } + return false; + } + + @Override + public boolean hasTasks(TaskSpan span) { + return span == this.span && this.hasTasks(); + } + + @Override + public boolean canHaveTasks(TaskSpan span) { + return span == this.span; + } + + @Override + public @Nullable Runnable poll(BaseThread currentThread) { + // Skip during server bootstrap or if there is no more time in the current spare time + if (MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking || !MinecraftServer.isConstructed) { + return null; + } + if (this.span.isYielding && !currentThread.canStartYieldingTasks) { + 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 @Nullable Runnable pollTiny(BaseThread currentThread) { + if (this.span == TaskSpan.TINY) { + return this.poll(currentThread); + } + return null; + } + + @Override + public void add(Runnable task, TaskSpan span) { + throw new UnsupportedOperationException(); + } + + /** + * To be called when a new task has been added to the underlying storage of this queue. + */ + public void newTaskWasAdded() { + BaseThreadActivation.newTaskWasAdded(this.tier, this.span, true, onlyNotifyBaseThreadPoolOfNewTasksIfLastTimeIsTooLongAgo); + } + + @Override + public void setTier(BaseTaskQueueTier tier) { + if (this.tier != null) { + throw new IllegalStateException(this.getClass().getSimpleName() + ".tier was already initialized"); + } + this.tier = tier; + } + +} 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..690979cb9b7ec3dedbd7d0c45d0c183a2f56d2ec --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/AnyTickScheduledServerThreadTaskQueue.java @@ -0,0 +1,27 @@ +// Gale - base thread pool + +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. + * + * @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/BaseTaskQueueTier.java b/src/main/java/org/galemc/gale/executor/queue/BaseTaskQueueTier.java new file mode 100644 index 0000000000000000000000000000000000000000..7e24854f1e727e5e40108c68933466d0845bdca1 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/BaseTaskQueueTier.java @@ -0,0 +1,127 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.queue; + +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.TaskSpan; +import org.galemc.gale.executor.thread.AssistThread; +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.ServerThread; + +import java.util.Arrays; + +/** + * A tier for {@link AbstractTaskQueue}s, that indicates the priority of the tasks in the task queues. + * Every tier contains a list of the queues that are part of the tier. + * The tiers are in order of priority, from high to low. + * Similarly, the queues for each tier are in the same order of priority. + * The tasks in each queue should also be in order of priority whenever relevant, but usually there + * is no strong difference in priority between tasks in the same queue, so they typically operate as FIFO queues, + * so that the longest waiting task implicitly has the highest priority within the queue. + *
    + * Tasks from queues in the {@link #SERVER} tier can only be run on a {@link ServerThread}. + * Tasks from other tiers can be run on {@link ServerThread}s as well as on {@link AssistThread}s. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public enum BaseTaskQueueTier { + + /** + * A tier for queues that contain tasks that must be executed on a {@link ServerThread}. + *
    + * 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. + *
    + * Note that a {@link ServerThread} can only yield to {@link TaskSpan#TINY} tasks in other tiers + * (since there are no higher tiers, and threads can only yield to lower tiers when + * the task yielded to is{@link TaskSpan#TINY}, or other non-yielding tasks in its own tier (since it + * has a {@link BaseThread#maximumYieldDepth} of 1). + * Yielding to other tasks in this same tier is somewhat risky, since this means that the tasks that were + * yielded to must assume that although they are running on the server thread, they may be running at + * some unknown point in execution of the main thread. Therefore, scheduling any non-yielding tasks to + * a queue in this tier must be done with the utmost care that the task cannot disrupt, or be disrupted by, + * the surrounding code that yields to it. + */ + SERVER(new AbstractTaskQueue[]{ + BaseTaskQueues.deferredToServerThread, + BaseTaskQueues.serverThreadTick, + BaseTaskQueues.anyTickScheduledServerThread + }, MinecraftServer.SERVER_THREAD_PRIORITY), + /** + * A tier for queues that contain tasks that are part of ticking, + * to assist the main ticking thread(s) in doing so. + */ + TICK_ASSIST(new AbstractTaskQueue[]{ + BaseTaskQueues.tickAssist + }, Integer.getInteger("gale.thread.priority.tick", 7)), + /** + * A tier for queues that contain general tasks that must be performed at some point in time, + * asynchronously with respect to the {@link ServerThread} and the ticking of the server. + * Execution of + */ + ASYNC(new AbstractTaskQueue[0], Integer.getInteger("gale.thread.priority.async", 6)), + /** + * A tier for queues that contain tasks with the same considerations as {@link #ASYNC}, + * but with a low priority. + */ + LOW_PRIORITY_ASYNC(new AbstractTaskQueue[0], Integer.getInteger("gale.thread.priority.async.low", 3)); + + /** + * Equal to {@link #ordinal()}. + */ + public final int ordinal; + + /** + * The task queues that belong to this tier. + */ + public final AbstractTaskQueue[] taskQueues; + + /** + * The priority for threads that are executing a task from this tier. + *
    + * If a thread yields to other tasks, the priority it will have is always the highest priority of any task + * on its stack. + */ + public final int threadPriority; + + BaseTaskQueueTier(AbstractTaskQueue[] taskQueues, int threadPriority) { + this.ordinal = this.ordinal(); + this.taskQueues = taskQueues; + for (AbstractTaskQueue queue : this.taskQueues) { + queue.setTier(this); + } + this.threadPriority = threadPriority; + } + + /** + * Equal to {@link #values()}. + */ + public static final BaseTaskQueueTier[] VALUES = values(); + + /** + * Equal to {@link #VALUES}{@code .length}. + */ + public static final int length = VALUES.length; + + /** + * Equal to {@link #VALUES} without {@link #SERVER}. + */ + public static final BaseTaskQueueTier[] VALUES_EXCEPT_SERVER = Arrays.stream(VALUES).filter(tier -> tier != SERVER).toList().toArray(new BaseTaskQueueTier[length - 1]); + +} 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..cf8e2b42ecfc8205af5b105e19975c3e54ffec5f --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/BaseTaskQueues.java @@ -0,0 +1,92 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.queue; + +import io.papermc.paper.util.MCUtil; +import io.papermc.paper.util.TickThread; +import org.galemc.gale.executor.TaskSpan; +import org.galemc.gale.executor.thread.BaseThread; +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 tasks of every {@link TaskSpan}. + *
    + * 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 SimpleTaskQueue deferredToServerThread = SimpleTaskQueue.allSpans("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 tasks of every {@link TaskSpan}. + *
    + * 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 SimpleTaskQueue deferredToUniversalTickThread = SimpleTaskQueue.allSpans("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}. + *
    + * This queue may contain tasks of every {@link TaskSpan}. + *
    + * 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 SimpleTaskQueue serverThreadTick = SimpleTaskQueue.allSpans("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 BaseThread}). + *
    + * This queue may contain tasks of every {@link TaskSpan}. + *
    + * Tasks in every queue are performed in the order they are added (first-in, first-out). Note that this means + * not all {@link BaseThread} tick tasks are necessarily performed in the order they are added, because they may be + * in different queues: either the queue for potentially yielding tasks or the queue for yield-free tasks. + */ + public static final SimpleTaskQueue tickAssist = SimpleTaskQueue.allSpans("TickAssist"); + +} 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..a763028deff9bc131b8208886bf2651373b1dbc3 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/ScheduledServerThreadTaskQueues.java @@ -0,0 +1,287 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.queue; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.thread.BlockableEventLoop; +import org.galemc.gale.concurrent.UnterminableExecutorService; +import org.galemc.gale.executor.annotation.Access; +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.Guarded; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.thread.pool.BaseThreadActivation; +import org.galemc.gale.executor.thread.ServerThread; +import org.galemc.gale.executor.TaskSpan; +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 {@link TaskSpan#YIELDING}: no special distinction for more + * permissive task spans 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; + + /** + * Will be initialized in {@link TickRequiredScheduledServerThreadTaskQueue#setTier}. + */ + static BaseTaskQueueTier tier; + + /** + * 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(); + + /** + * @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) { + while (!readLock.tryLock()) { + Thread.onSpinWait(); + } + try { + // Try the queue most likely to contain tasks first + if (firstQueueWithPotentialTasksIndex == 0 || tryNonCurrentTickQueuesAtAll) { + if (!queues[firstQueueWithPotentialTasksIndex].isEmpty()) { + return true; + } + } + int checkUpTo = tryNonCurrentTickQueuesAtAll ? 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.canStartYieldingTasks) { + 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() { + while (!readLock.tryLock()) { + Thread.onSpinWait(); + } + 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 + while (!readLock.tryLock()) { + Thread.onSpinWait(); + } + try { + //noinspection NonAtomicOperationOnVolatileField + versionStamp++; + //noinspection unchecked + queues[maxDelay].add(task); + if (maxDelay < firstQueueWithPotentialTasksIndex) { + firstQueueWithPotentialTasksIndex = maxDelay; + } + //noinspection NonAtomicOperationOnVolatileField + versionStamp++; + } finally { + readLock.unlock(); + } + MinecraftServer.nextTimeAssumeWeMayHaveDelayedTasks = true; + BaseThreadActivation.newTaskWasAdded(tier, TaskSpan.YIELDING); + } + + /** + * This moves the queues in {@link #queues}. + */ + public static void shiftTasksForNextTick() { + while (!writeLock.tryLock()) { + Thread.onSpinWait(); + } + 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..2c910f89f1056d00e5e4a2d832cdd4be4b7527b4 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/SimpleTaskQueue.java @@ -0,0 +1,253 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.queue; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import org.galemc.gale.collection.FIFOConcurrentLinkedQueue; +import org.galemc.gale.concurrent.UnterminableExecutorService; +import org.galemc.gale.executor.TaskSpan; +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.pool.BaseThreadActivation; +import org.galemc.gale.executor.thread.SignalReason; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.Queue; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; + +/** + * A base class for a task queue that may contain tasks that are + * potentially yielding and tasks that are yield-free. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@AnyThreadSafe +@YieldFree +public class SimpleTaskQueue implements AbstractTaskQueue { + + /** + * The name of this queue. + * + * @see #getName() + */ + public final String name; + + /** + * The {@link BaseTaskQueueTier} that contains this queue. + * Will be initialized in {@link #setTier} by the constructor of the tier. + */ + public BaseTaskQueueTier tier; + + /** + * Whether tasks in this queue can have a {@link TaskSpan}, indexed by the span's {@link TaskSpan#ordinal}. + */ + public final boolean[] canHaveTasks; + + /** + * The queues of tasks, indexed by their {@link TaskSpan#ordinal}. Individual elements may be null if the + * corresponding {@link #canHaveTasks} value is false. + */ + @SuppressWarnings("rawtypes") + private final @Nullable Queue @NotNull [] queues; + + /** + * An executor for adding tasks of a specific {@link TaskSpan} to this queue, where {@link Executor#execute} + * calls {@link #add(Runnable, TaskSpan)}, + * or null if the corresponding value in {@link #canHaveTasks} is false. + */ + @SuppressWarnings("FieldCanBeLocal") + private final @Nullable ExecutorService @NotNull [] executors; + + /** + * An executor for adding {@link TaskSpan#YIELDING} tasks to this queue, + * where {@link Executor#execute} calls {@link #add}, + * or null if the corresponding value in {@link #canHaveTasks} is false. + */ + public final ExecutorService yieldingExecutor; + + /** + * An executor for adding {@link TaskSpan#FREE} tasks to this queue, + * where {@link Executor#execute} calls {@link #add}, + * or null if the corresponding value in {@link #canHaveTasks} is false. + */ + public final ExecutorService freeExecutor; + + /** + * An executor for adding {@link TaskSpan#TINY} tasks to this queue, + * where {@link Executor#execute} calls {@link #add}, + * or null if the corresponding value in {@link #canHaveTasks} is false. + */ + public final ExecutorService tinyExecutor; + + SimpleTaskQueue(String name, boolean[] canHaveTasks) { + this(name, canHaveTasks, false); + } + + /** + * @param name Value for {@link #getName}. + * @param canHaveTasks Value for {@link #canHaveTasks}. + * @param lifoQueues If true, the queues in {@link #queues} will be LIFO; + * otherwise, they will be FIFO. + */ + SimpleTaskQueue(String name, boolean[] canHaveTasks, boolean lifoQueues) { + this.name = name; + if (canHaveTasks.length != TaskSpan.length) { + throw new IllegalArgumentException(); + } + this.canHaveTasks = canHaveTasks; + this.queues = new Queue[TaskSpan.length]; + this.executors = new ExecutorService[TaskSpan.length]; + for (int spanOrdinal = 0; spanOrdinal < TaskSpan.length; spanOrdinal++) { + if (this.canHaveTasks[spanOrdinal]) { + this.queues[spanOrdinal] = lifoQueues ? new FIFOConcurrentLinkedQueue<>() : new MultiThreadedQueue<>(); + this.executors[spanOrdinal] = new SpanExecutor(TaskSpan.VALUES[spanOrdinal]); + } + } + this.yieldingExecutor = this.executors[TaskSpan.YIELDING.ordinal]; + this.freeExecutor = this.executors[TaskSpan.FREE.ordinal]; + this.tinyExecutor = this.executors[TaskSpan.TINY.ordinal]; + } + + @Override + public final String getName() { + return this.name; + } + + @Override + public final boolean hasTasks() { + for (int spanOrdinal = 0; spanOrdinal < TaskSpan.length; spanOrdinal++) { + var queue = this.queues[spanOrdinal]; + if (queue != null && !queue.isEmpty()) { + return true; + } + } + return false; + } + + @Override + public boolean hasTasks(TaskSpan span) { + var queue = this.queues[span.ordinal]; + return queue != null && !queue.isEmpty(); + } + + @Override + public boolean canHaveTasks(TaskSpan span) { + return this.queues[span.ordinal] != null; + } + + /** + * @see #poll(BaseThread) + */ + private @Nullable Runnable poll(BaseThread currentThread, int spanOrdinal) { + var queue = this.queues[spanOrdinal]; + if (queue != null) { + if (currentThread.canStartYieldingTasks || TaskSpan.VALUES[spanOrdinal].isNotYielding) { + Object task = queue.poll(); + if (task != null) { + /* + If the thread was woken up for a different reason, + another thread should be signalled for that reason. + */ + SignalReason lastSignalReason = currentThread.lastSignalReason; + if (lastSignalReason != null && lastSignalReason != SignalReason.TASK) { + BaseThreadActivation.callForUpdate(); + } + return (Runnable) task; + } + } + } + return null; + } + + @Override + public final @Nullable Runnable poll(BaseThread currentThread) { + Runnable task; + for (int spanOrdinal = 0; spanOrdinal < TaskSpan.length; spanOrdinal++) { + task = this.poll(currentThread, spanOrdinal); + if (task != null) { + return task; + } + } + return null; + } + + @Override + public final @Nullable Runnable pollTiny(BaseThread currentThread) { + return poll(currentThread, TaskSpan.TINY.ordinal); + } + + @Override + public final void add(Runnable task, TaskSpan span) { + int spanOrdinal = span.ordinal; + //noinspection unchecked + this.queues[spanOrdinal].add(task); + BaseThreadActivation.newTaskWasAdded(this.tier, span); + } + + @Override + public final void setTier(BaseTaskQueueTier tier) { + if (this.tier != null) { + throw new IllegalStateException("SimpleTaskQueue.tier was already initialized"); + } + this.tier = tier; + } + + private class SpanExecutor extends UnterminableExecutorService { + + private final TaskSpan span; + + private SpanExecutor(TaskSpan span) { + this.span = span; + } + + @Override + public void execute(@NotNull Runnable runnable) { + SimpleTaskQueue.this.add(runnable, this.span); + } + + } + + public static SingleSpanSimpleTaskQueue singleSpan(String name, TaskSpan span) { + return new SingleSpanSimpleTaskQueue(name, span); + } + + @SuppressWarnings("unused") + public static SingleSpanSimpleTaskQueue singleSpan(String name, TaskSpan span, boolean lifoQueues) { + return new SingleSpanSimpleTaskQueue(name, span, lifoQueues); + } + + private static boolean[] createCanHaveTasksForDoubleSpan(TaskSpan span1, TaskSpan span2) { + boolean[] canHaveTasks = new boolean[TaskSpan.length]; + canHaveTasks[span1.ordinal] = true; + canHaveTasks[span2.ordinal] = true; + return canHaveTasks; + } + + @SuppressWarnings("unused") + public static SimpleTaskQueue doubleSpan(String name, TaskSpan span1, TaskSpan span2) { + return new SimpleTaskQueue(name, createCanHaveTasksForDoubleSpan(span1, span2)); + } + + @SuppressWarnings("unused") + public static SimpleTaskQueue doubleSpan(String name, TaskSpan span1, TaskSpan span2, boolean lifoQueues) { + return new SimpleTaskQueue(name, createCanHaveTasksForDoubleSpan(span1, span2), lifoQueues); + } + + private static final boolean[] canHaveTasksForAllSpans = new boolean[TaskSpan.length]; + static { + Arrays.fill(canHaveTasksForAllSpans, true); + } + + public static SimpleTaskQueue allSpans(String name) { + return new SimpleTaskQueue(name, canHaveTasksForAllSpans); + } + + public static SimpleTaskQueue allSpans(String name, boolean lifoQueues) { + return new SimpleTaskQueue(name, canHaveTasksForAllSpans, lifoQueues); + } + +} diff --git a/src/main/java/org/galemc/gale/executor/queue/SingleSpanSimpleTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/SingleSpanSimpleTaskQueue.java new file mode 100644 index 0000000000000000000000000000000000000000..2251087670d554a7bd5dc81631615aa0728eb315 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/SingleSpanSimpleTaskQueue.java @@ -0,0 +1,66 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.queue; + +import org.galemc.gale.concurrent.UnterminableExecutorService; +import org.galemc.gale.executor.TaskSpan; +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 of one {@link TaskSpan}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@YieldFree +public class SingleSpanSimpleTaskQueue extends SimpleTaskQueue { + + private static final boolean[][] canHaveTasksPerSpan = new boolean[TaskSpan.length][]; + static { + for (TaskSpan span : TaskSpan.VALUES) { + canHaveTasksPerSpan[span.ordinal] = new boolean[TaskSpan.length]; + canHaveTasksPerSpan[span.ordinal][span.ordinal] = true; + } + } + + private final TaskSpan span; + + SingleSpanSimpleTaskQueue(String name, TaskSpan span) { + super(name, canHaveTasksPerSpan[span.ordinal]); + this.span = span; + } + + SingleSpanSimpleTaskQueue(String name, TaskSpan span, boolean lifoQueues) { + super(name, canHaveTasksPerSpan[span.ordinal], lifoQueues); + this.span = span; + } + + /** + * Schedules a new task to this queue. + * + * @param task The task to schedule. + */ + @AnyThreadSafe + @YieldFree + public void add(Runnable task) { + this.add(task, this.span); + } + + /** + * 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) { + SingleSpanSimpleTaskQueue.this.add(runnable, SingleSpanSimpleTaskQueue.this.span); + } + + }; + +} 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..3cb2b84cb7653ff3e038acdc2e6e11f805bfbbba --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/ThisTickScheduledServerThreadTaskQueue.java @@ -0,0 +1,27 @@ +// Gale - base thread pool + +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. + * + * @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..fb4f9c047fc71a9a01aa47871254c6a949753a3a --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/TickRequiredScheduledServerThreadTaskQueue.java @@ -0,0 +1,67 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.queue; + +import org.galemc.gale.executor.TaskSpan; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.ServerThread; +import org.jetbrains.annotations.Nullable; + +/** + * A common base class for {@link ThisTickScheduledServerThreadTaskQueue} and + * {@link AnyTickScheduledServerThreadTaskQueue}. + *
    + * This queue does not support {@link #add}. + * + * @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 hasTasks() { + return ScheduledServerThreadTaskQueues.hasTasks(this.tryNonCurrentTickQueuesAtAll); + } + + @Override + public boolean hasTasks(TaskSpan span) { + return span == TaskSpan.YIELDING && this.hasTasks(); + } + + @Override + public boolean canHaveTasks(TaskSpan span) { + return span == TaskSpan.YIELDING; + } + + @Override + public @Nullable Runnable poll(BaseThread currentThread) { + return ScheduledServerThreadTaskQueues.poll((ServerThread) currentThread, this.tryNonCurrentTickQueuesAtAll); + } + + @Override + public @Nullable Runnable pollTiny(BaseThread currentThread) { + return null; + } + + @Override + public void add(Runnable task, TaskSpan span) { + throw new UnsupportedOperationException(); + } + + @Override + public void setTier(BaseTaskQueueTier tier) { + if (ScheduledServerThreadTaskQueues.tier == null) { + ScheduledServerThreadTaskQueues.tier = tier; + } + } + +} 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..fd4082f1d8fe558bbaaf3b17da18b361e34c81ed --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/AbstractYieldingThread.java @@ -0,0 +1,147 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.thread; + +import net.minecraft.server.MinecraftServer; +import org.galemc.gale.concurrent.CheckableLock; +import org.galemc.gale.executor.annotation.PotentiallyYielding; +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.annotation.thread.ThisThreadOnly; +import org.galemc.gale.executor.lock.SingleWaitingBaseThreadYieldingLock; +import org.galemc.gale.executor.lock.YieldingLock; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; + +/** + * An interface for threads that can yield to other tasks, for example upon encountering a {@link YieldingLock}, + * in lieu of blocking. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public interface AbstractYieldingThread extends SignallableThread { + + /** + * @return Whether this thread currently holds any {@link YieldingLock}. + */ + @ThisThreadOnly + @YieldFree + boolean holdsYieldingLock(); + + /** + * Increments the number of times this thread is holding a {@link YieldingLock}. + * A thread can hold one {@link YieldingLock} multiple times (it can be reentrant). + */ + @ThisThreadOnly + @YieldFree + void incrementHeldYieldingLockCount(); + + /** + * Decrements the number of times this thread is holding a {@link YieldingLock}. + * + * @see #incrementHeldYieldingLockCount() + */ + @ThisThreadOnly + @YieldFree + void decrementHeldYieldingLockCount(); + + @ThisThreadOnly + @PotentiallyYielding("this method is meant to yield") + void yieldUntil(@Nullable Long timeoutTime, @Nullable BooleanSupplier stopCondition, @Nullable YieldingLock yieldingLock); + + /** + * Calls {@link #yieldUntil(BooleanSupplier, YieldingLock)}, but creates a {@link YieldingLock} + * based on the given {@code future}, that causes this thread to be notified when the future is completed. + * + * @param autoCompletingLockConsumer An optional consumer that will be applied to the created {@link YieldingLock}, + * so that {@link YieldingLock#unlock} can be called on it + * when the {@code stopCondition} becomes true. + */ + @ThisThreadOnly + @PotentiallyYielding("this method is meant to yield") + default void yieldUntilFuture(@Nullable Long timeoutTime, @Nullable BooleanSupplier stopCondition, @NotNull CompletableFuture future, @Nullable Consumer autoCompletingLockConsumer) { + + /* + Here, we abuse the Lock interface to create a YieldingLock + that can be speculatively locked only while tasks for this blockable event loop are available, + or when the future has completed. + */ + YieldingLock autoCompletingLock = new SingleWaitingBaseThreadYieldingLock(new CheckableLock() { + + @Override + public boolean isLocked() { + return !stopCondition.getAsBoolean() && !future.isDone(); + } + + @Override + public boolean isHeldByCurrentThread() { + throw new UnsupportedOperationException("isHeldByCurrentThread() is not supported for a yieldUntilFuture lock"); + } + + @Override + public void lock() { + throw new UnsupportedOperationException("lock() is not supported for a yieldUntilFuture lock"); + } + + @Override + public void lockInterruptibly() { + throw new UnsupportedOperationException("lockInterruptibly() is not supported for a yieldUntilFuture lock"); + } + + @Override + public boolean tryLock() { + return !this.isLocked(); + } + + @Override + public boolean tryLock(long time, @NotNull TimeUnit unit) throws InterruptedException { + throw new UnsupportedOperationException("tryLock(long, TimeUnit) is not supported for a yieldUntilFuture lock"); + } + + @Override + public void unlock() {} + + @NotNull + @Override + public Condition newCondition() { + throw new UnsupportedOperationException("newCondition() is not supported for a yieldUntilFuture lock"); + } + + }).withCanNotBeHeld(); + if (autoCompletingLockConsumer != null) { + autoCompletingLockConsumer.accept(autoCompletingLock); + } + // Be properly notified when the future completes + future.thenRun(autoCompletingLock::unlock); + + // Yield while necessary + this.yieldUntil(timeoutTime, null, autoCompletingLock); + + } + + @ThisThreadOnly + void runTasksUntil(@Nullable Long timeoutTime, @Nullable BooleanSupplier stopCondition, @Nullable YieldingLock yieldingLock); + + @AnyThreadSafe + @YieldFree + static @Nullable AbstractYieldingThread currentYieldingThread() { + return Thread.currentThread() instanceof AbstractYieldingThread yieldingThread ? yieldingThread : null; + } + + /** + * @return Whether the current thread is a {@link AbstractYieldingThread}. + */ + @SuppressWarnings("unused") + @AnyThreadSafe + @YieldFree + static boolean isYieldingThread() { + return Thread.currentThread() instanceof AbstractYieldingThread; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/AssistThread.java b/src/main/java/org/galemc/gale/executor/thread/AssistThread.java new file mode 100644 index 0000000000000000000000000000000000000000..eab769d7319f26db1f4db9599a3c263c507641bd --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/AssistThread.java @@ -0,0 +1,78 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.thread; + +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.thread.BaseThreadOnly; +import org.galemc.gale.executor.annotation.thread.ThisThreadOnly; +import org.galemc.gale.executor.thread.pool.BaseThreadPool; +import org.jetbrains.annotations.Nullable; + +/** + * A thread created by the {@link BaseThreadPool}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public class AssistThread extends BaseThread { + + /** + * The maximum yield depth. While an {@link AssistThread} has a yield depth equal to or greater than this value, + * it can not start more potentially yielding tasks. + */ + public static final int MAXIMUM_YIELD_DEPTH = Integer.getInteger("gale.yield.depth.max", 100); + + /** + * The index of this thread, as needed as an argument to + * {@link BaseThreadPool#getThreadByAssistIndex(int)}. + */ + public final int assistThreadIndex; + + /** + * Must only be called from {@link BaseThreadPool#addAssistThread}. + */ + public AssistThread(int assistThreadIndex) { + super(AssistThread::getCurrentAssistThreadAndRunForever, "Assist Thread " + assistThreadIndex, assistThreadIndex + 1, MAXIMUM_YIELD_DEPTH); + this.assistThreadIndex = assistThreadIndex; + } + + /** + * 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(null, () -> false, null); + } + + /** + * @return The current thread if it is a {@link AssistThread}, or null otherwise. + */ + @SuppressWarnings("unused") + @AnyThreadSafe + @YieldFree + public static @Nullable AssistThread currentAssistThread() { + return Thread.currentThread() instanceof AssistThread assistThread ? assistThread : null; + } + + /** + * @return Whether the current thread is a {@link AssistThread}. + */ + @SuppressWarnings("unused") + @AnyThreadSafe + @YieldFree + public static boolean isAssistThread() { + return Thread.currentThread() instanceof AssistThread; + } + + /** + * A method that simply acquires the {@link AssistThread} that is the current thread, and calls + * {@link #runForever()} on it. + */ + @BaseThreadOnly + protected static void getCurrentAssistThreadAndRunForever() { + ((AssistThread) Thread.currentThread()).runForever(); + } + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/BaseThread.java b/src/main/java/org/galemc/gale/executor/thread/BaseThread.java new file mode 100644 index 0000000000000000000000000000000000000000..4ec06cbff045bd42c1da5881cd0f2446cde39a7b --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/BaseThread.java @@ -0,0 +1,765 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.thread; + +import io.papermc.paper.util.TickThread; +import net.minecraft.server.MinecraftServer; +import org.galemc.gale.executor.TaskSpan; +import org.galemc.gale.executor.annotation.Access; +import org.galemc.gale.executor.annotation.PotentiallyYielding; +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.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.queue.BaseTaskQueueTier; +import org.galemc.gale.executor.thread.pool.*; +import org.jetbrains.annotations.Nullable; + +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; + +/** + * An abstract base class implementing {@link AbstractYieldingThread}, + * that provides implementation that is common between + * {@link TickThread} and {@link AssistThread}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public abstract class BaseThread 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 index of this thread, as needed as an argument to + * {@link BaseThreadPool#getThreadByBaseIndex(int)}. + */ + public final int baseThreadIndex; + + /** + * The maximum yield depth for this thread, + * which equals 1 for a {@link ServerThread} + * and {@link AssistThread#MAXIMUM_YIELD_DEPTH} for an {@link AssistThread}. + */ + public final int maximumYieldDepth; + + /** + * The number of times this thread holds a {@link YieldingLock}, + * used in {@link #holdsYieldingLock()}. + * + * @see AbstractYieldingThread#incrementHeldYieldingLockCount() + */ + @ThisThreadOnly + public int heldYieldingLockCount = 0; + + /** + * The current yield depth of this thread. + */ + @AnyThreadSafe(Access.READ) @ThisThreadOnly(Access.WRITE) + public volatile int yieldDepth = 0; + + /** + * Whether this thread can currently start yielding tasks with respect to being restricted + * due to {@link #yieldDepth} being at least {@link #maximumYieldDepth}. + *
    + * This is updated using {@link #updateCanStartYieldingTasks()} + * after {@link #yieldDepth} or {@link #heldYieldingLockCount} is changed. + */ + @AnyThreadSafe(Access.READ) @ThisThreadOnly(Access.WRITE) + public volatile boolean canStartYieldingTasks = true; + + /** + * The highest {@link BaseTaskQueueTier} of any task on the yielding execution stack of this thread, + * or null if there is no task being executed on this thread. + */ + @AnyThreadSafe(Access.READ) @ThisThreadOnly(Access.WRITE) + public volatile @Nullable BaseTaskQueueTier highestTierOfTaskOnStack; + + /** + * The {@link BaseTaskQueueTier} that the last non-null return value of {@link #pollTask} was polled from, + * or null if {@link #pollTask} has never been called yet. + */ + @ThisThreadOnly + private @Nullable BaseTaskQueueTier lastPolledTaskTier; + + /** + * The lock to guard this thread's sleeping and waking actions. + */ + private final Lock waitLock = new ReentrantLock(); + + /** + * The condition to wait for a signal, when this thread has to wait for something to do. + */ + private final Condition waitCondition = waitLock.newCondition(); + + /** + * Whether this thread is currently not working on the content of a task, but instead + * attempting to poll a next task to do, checking whether it can accept tasks at all, or + * attempting to acquire a {@link YieldingLock}, or waiting (although the fact that this value is true during + * waiting is irrelevant, because at such a time, {@link #isWaiting} will be true, and this value will no longer + * have any effect due to the implementation of {@link #signal}). + *
    + * This value is used to determine whether to set {@link #skipNextWait} when {@link #signal} is called + * and {@link #isWaiting} is false. + */ + @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 + public volatile boolean skipNextWait = false; + + /** + * Whether this thread is currently waiting for something to do. + *
    + * This is set to true at some point before actually starting to wait in a blocking fashion, + * and set to false at some point after no longer waiting in a blocking fashion. So, at some point, + * this value may be true while the thread is not blocked yet, or anymore. + * Even more so, extra checks for whether the thread should block will be performed in between + * the moment this value is set to true and the moment the thread potentially blocks. This means that if the + * checks fail, this value may be set to true and then false again, without actually ever blocking. + */ + @AnyThreadSafe(Access.READ) @ThisThreadOnly(Access.WRITE) + @Guarded(value = "#waitLock", fieldAccess = Access.WRITE) + public volatile boolean isWaiting = false; + + /** + * Whether {@link #isWaiting} is irrelevant because this thread has already + * been signalled via {@link #signal} to wake up. + */ + @AnyThreadSafe(Access.READ) @ThisThreadOnly(Access.WRITE) + @Guarded(value = "#waitLock", fieldAccess = Access.WRITE) + public volatile boolean mayBeStillWaitingButHasBeenSignalled = false; + + /** + * The {@link YieldingLock} that this thread is waiting for, + * or null if this thread is not waiting for a {@link YieldingLock}. + * This value only has meaning while {@link #isWaiting} is true. + */ + @AnyThreadSafe(Access.READ) @ThisThreadOnly(Access.WRITE) + @Guarded(value = "#waitLock", fieldAccess = Access.WRITE) + public volatile @Nullable YieldingLock lockWaitingFor = null; + + /** + * The value of {@link #lockWaitingFor} during the last wait (a call to {@link Condition#await}) + * or pre-wait check (while {@link #isNotActuallyWaitingYet} is true). + */ + @ThisThreadOnly + private @Nullable YieldingLock lastLockWaitedFor = null; + + /** + * A special flag, used after changing {@link #isWaiting}, when the lock must be temporarily released to + * call {@link BaseThreadActivation#callForUpdate()} (to avoid deadlocks in {@link #signal} calls), + * and we wish the pool to regard this thread as waiting + * (which it will, because {@link #isWaiting} will be true), but we must still + * know not to signal the underlying {@link #waitCondition}, but set {@link #skipNextWait} to true, + * when {@link #signal} is called at some point during the short release of {@link #waitLock}. + */ + public volatile boolean isNotActuallyWaitingYet = false; + + /** + * The last reason this thread was signalled before the current poll attempt, or null if the current + * poll attempt was not preceded by signalling (but by yielding for example). + */ + public volatile @Nullable SignalReason lastSignalReason = null; + + protected BaseThread(Runnable target, String name, int baseThreadIndex, int maximumYieldDepth) { + super(target, name); + this.baseThreadIndex = baseThreadIndex; + this.maximumYieldDepth = maximumYieldDepth; + } + + @Override + public boolean holdsYieldingLock() { + return this.heldYieldingLockCount > 0; + } + + @Override + public void incrementHeldYieldingLockCount() { + this.heldYieldingLockCount++; + if (this.heldYieldingLockCount == 1) { + this.updateCanStartYieldingTasks(); + } + } + + @Override + public void decrementHeldYieldingLockCount() { + this.heldYieldingLockCount--; + if (this.heldYieldingLockCount == 0) { + this.updateCanStartYieldingTasks(); + } + } + + /** + * Updates {@link #canStartYieldingTasks} according to {@link #yieldDepth} and {@link #heldYieldingLockCount}. + */ + private void updateCanStartYieldingTasks() { + this.canStartYieldingTasks = this.heldYieldingLockCount == 0 && this.yieldDepth < this.maximumYieldDepth; + } + + /** + * This method is based on {@link #signal}. + * {@link #signal} must always return true if this method returns true; + * otherwise {@link BaseThreadActivation} will get stuck while choosing a thread to activate. + * + * @see #signal + */ + @SuppressWarnings("RedundantIfStatement") + public boolean isWaitingAndNeedsSignal() { + if (this.isWaiting) { + if (this.isNotActuallyWaitingYet) { + if (!this.skipNextWait) { + return true; + } + return false; + } + if (!this.mayBeStillWaitingButHasBeenSignalled) { + return true; + } + } + return false; + } + + /** + * 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. + */ + public final void yieldUntil(@Nullable Long timeoutTime, @Nullable BooleanSupplier stopCondition, @Nullable YieldingLock yieldingLock) { + int oldYieldDepth = this.yieldDepth; + int newYieldDepth = oldYieldDepth + 1; + this.yieldDepth = newYieldDepth; + if (newYieldDepth == maximumYieldDepth) { + this.updateCanStartYieldingTasks(); + } + this.runTasksUntil(timeoutTime, stopCondition, yieldingLock); + this.yieldDepth = oldYieldDepth; + if (newYieldDepth == maximumYieldDepth) { + this.updateCanStartYieldingTasks(); + } + } + + /** + * This method will keep attempting to find a task to do, and execute it, and if none is found, start waiting + * until the {@code timeoutTime} is reached (which is compared to {@link System#nanoTime}), + * or the thread is signalled by {@link BaseThreadPool} or by a {@link YieldingLock}. + * 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 n{@link AssistThread}. The difference with + * {@link #yieldUntil} is that this method does not increment or decrement things the yield depth of this thread. + *
    + * Exactly one of {@code stopCondition} or {@code yieldingLock} must be non-null. + * + * @see #yieldUntil + */ + @ThisThreadOnly + @PotentiallyYielding("may yield further if an executed task is potentially yielding") + public final void runTasksUntil(@Nullable Long timeoutTime, @Nullable BooleanSupplier stopCondition, @Nullable YieldingLock yieldingLock) { + if (TickThread.isTickThread()) MinecraftServer.THREAD_DEBUG_LOGGER.ifPresent(it -> it.info("running tasks until")); + 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) { + try { + if (timeoutTime != null && System.nanoTime() - timeoutTime >= 0) { + break; + } + 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; + } + } + } finally { + // Make sure other threads can be signalled for the last waited-for lock again + if (this.lastLockWaitedFor != null) { + this.lastLockWaitedFor.canBeSignalledFor = true; + this.lastLockWaitedFor = null; + } + } + + // If this is the original 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(BaseTaskQueueTier.SERVER.taskQueues)) { + MinecraftServer.nextTimeAssumeWeMayHaveDelayedTasks = true; + } + + // Update highestTierOfTaskOnStack and the thread priority + var highestTierBeforeTask = this.highestTierOfTaskOnStack; + var threadPriorityBeforeTask = this.getPriority(); + //noinspection DataFlowIssue + var newHighestTier = highestTierBeforeTask == null ? this.lastPolledTaskTier : highestTierBeforeTask.ordinal < this.lastPolledTaskTier.ordinal ? highestTierBeforeTask : this.lastPolledTaskTier; + //noinspection DataFlowIssue + var newThreadPriority = newHighestTier.threadPriority; + if (newHighestTier != highestTierBeforeTask) { + this.highestTierOfTaskOnStack = newHighestTier; + BaseThreadActivation.callForUpdate(); + if (threadPriorityBeforeTask != newThreadPriority) { + this.setPriority(newThreadPriority); + } + } + + this.isPollingTaskOrCheckingStopCondition = false; + task.run(); + + // If this is the server thread, execute some chunk tasks + if (this == MinecraftServer.serverThread) { + if (newHighestTier != BaseTaskQueueTier.SERVER) { + newHighestTier = BaseTaskQueueTier.SERVER; + this.highestTierOfTaskOnStack = newHighestTier; + BaseThreadActivation.callForUpdate(); + if (newThreadPriority != newHighestTier.threadPriority) { + newThreadPriority = newHighestTier.threadPriority; + this.setPriority(newThreadPriority); + } + } + MinecraftServer.SERVER.executeMidTickTasks(); // Paper - execute chunk tasks mid tick + } + + // Reset highestTierOfTaskOnStack and the thread priority + if (newHighestTier != highestTierBeforeTask) { + this.highestTierOfTaskOnStack = highestTierBeforeTask; + BaseThreadActivation.callForUpdate(); + if (threadPriorityBeforeTask != newThreadPriority) { + this.setPriority(threadPriorityBeforeTask); + } + } + + this.isPollingTaskOrCheckingStopCondition = true; + continue; + + } + + /* + If no task that can be started by this thread was found, wait for a task that we are allowed + to poll to become available (when that happens, the BaseThreadPool will signal this thread), + 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(timeoutTime, yieldingLock); + + } + + 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 for that reason. + */ + SignalReason lastSignalReason = this.lastSignalReason; + if (lastSignalReason != null && yieldingLock != null && lastSignalReason != SignalReason.YIELDING_LOCK) { + BaseThreadActivation.callForUpdate(); + } + + } + + /** + * @see #pollTask() + */ + @ThisThreadOnly + @YieldFree + private @Nullable Runnable pollTaskFromTier(BaseTaskQueueTier tier, boolean tinyOnly) { + for (var queue : tier.taskQueues) { + // Check whether we can not yield to the queue, if we are yielding + boolean canQueueBeYieldedTo = queue.canBeYieldedTo(); + if (!canQueueBeYieldedTo && this.yieldDepth > 0) { + continue; + } + Runnable task = tinyOnly ? queue.pollTiny(this) : queue.poll(this); + if (task != null) { + this.lastPolledTaskTier = tier; + return task; + } + /* + Check if the tier has run out of tasks for a span, + in order to update BaseThreadActivation#thereMayBeTasks. + */ + for (int spanI = 0; spanI < TaskSpan.length; spanI++) { + TaskSpan span = TaskSpan.VALUES[spanI]; + if (queue.canHaveTasks(span)) { + int oldTasks = BaseThreadActivation.thereMayBeTasks[tier.ordinal][spanI][canQueueBeYieldedTo ? 1 : 0].get(); + if (oldTasks > 0) { + if (!queue.hasTasks(span)) { + boolean tierHasNoTasksForSpan = true; + for (AbstractTaskQueue otherTierQueue : tier.taskQueues) { + // We already know there are no tasks in this queue + if (otherTierQueue == queue) { + continue; + } + if (otherTierQueue.hasTasks(span)) { + tierHasNoTasksForSpan = false; + break; + } + } + if (tierHasNoTasksForSpan) { + // Set thereMayBeTasks to false, but only if it did not change in the meantime + BaseThreadActivation.thereMayBeTasks[tier.ordinal][spanI][canQueueBeYieldedTo ? 1 : 0].compareAndSet(oldTasks, 0); + } + } + } + } + } + } + return null; + } + + /** + * 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 + private @Nullable Runnable pollTask() { + /* + * If this is a server thread, poll from SERVER, and poll tiny tasks from other tiers. + * Note that when polling on the ServerThread, we do not check whether we would be allowed to do so + * by the BaseThreadPool, as we consider keeping the ServerThread in the Thread.State.RUNNABLE state for + * as long as possible to be more important than the off-chance of for example starting a TINY ASYNC task + * on the server thread while no ASYNC tasks are allowed to be polled by other threads at the moment. + */ + if (this instanceof ServerThread) { + // Poll from the SERVER queues + Runnable task = this.pollTaskFromTier(BaseTaskQueueTier.SERVER, false); + if (task != null) { + return task; + } + // Poll tiny tasks from other tiers + for (var tier : BaseTaskQueueTier.VALUES_EXCEPT_SERVER) { + task = this.pollTaskFromTier(tier, true); + if (task != null) { + return task; + } + } + // We failed to poll any task + return null; + } + // If this is not a server thread, poll from all queues except SERVER + for (var tier : BaseTaskQueueTier.VALUES_EXCEPT_SERVER) { + /* + Make sure that we are allowed to poll from the tier, according to the presence of an excess number of + threads working on tasks from that tier during the last BaseThreadActivation#update call. + In the case this check's result is too optimistic, and a task is started when ideally it wouldn't have been, + then so be it - it is not terrible. Whenever this happens, enough threads will surely be allocated + by the BaseThreadPool for the task tier that is more in demand anyway, so it does not matter much. + In the case this check's result is too pessimistic, the polling fails and this thread will start to sleep, + but before doing this, will make a call to BaseThreadActivation#callForUpdate that re-activated this + thread if necessary, so no harm is done. + In the case this check causes this thread to go to sleep, the call to BaseThreadActivation#callForUpdate + while isWaiting is true will make sure the BaseThreadPool has the ability to correctly activate a + different thread (that is able to start tasks of a higher tier) if needed. + Here, we do not even make an exception for TINY tasks, since there may already be ongoing avoidable + context-switching due to excess threads that we can solve by letting this thread go to sleep. + */ + if (tier.ordinal < BaseThreadActivation.tierInExcessOrdinal) { + /* + Tasks of a certain tier may yield to tasks of the same or a higher + tier, and they may also yield to tiny tasks of a lower tier. + */ + var tierYieldingFrom = this.highestTierOfTaskOnStack; + Runnable task = this.pollTaskFromTier(tier, tierYieldingFrom != null && tier.ordinal > tierYieldingFrom.ordinal); + if (task != null) { + return task; + } + } + } + // We failed to poll any task + return null; + } + + /** + * Starts waiting on something to do. + * + * @param timeoutTime The maximum time to wait until (compared to {@link System#nanoTime}). + * @param yieldingLock A {@link YieldingLock} to register with, or null if this thread is not waiting for + * a yielding lock. + */ + @ThisThreadOnly + @PotentiallyBlocking + private void waitUntilSignalled(@Nullable Long timeoutTime, @Nullable YieldingLock yieldingLock) { + + // Remember whether we registered to wait with the lock, to unregister later + // Register this thread with the lock if necessary + boolean registeredAsWaitingWithLock = false; + if (yieldingLock != null) { + // No point in registering if we're not going to wait anyway + if (!this.skipNextWait) { + yieldingLock.incrementWaitingThreads(); + registeredAsWaitingWithLock = true; + } + } + + /* + Remember whether we changed anything that requires a BaseThreadPool#update call + (after the last call to that method). + */ + boolean mustCallPoolUpdateAtEnd = false; + + /* + 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; + } + + // Mark this thread as waiting + this.lockWaitingFor = yieldingLock; + this.mayBeStillWaitingButHasBeenSignalled = false; + this.isWaiting = true; + // But actually we are not waiting yet, signal has no effect yet during the next short lock release + this.isNotActuallyWaitingYet = true; + + } finally { + this.waitLock.unlock(); + } + + // Update the pool + BaseThreadActivation.callForUpdate(); + + /* + If we cannot acquire the lock, we can assume this thread is being signalled, + so there is no reason to start waiting. + */ + if (this.waitLock.tryLock()) { + try { + + // We passed the short lock release + this.isNotActuallyWaitingYet = false; + + // If it was set that this thread should skip the wait in the meantime, skip it + if (this.skipNextWait) { + this.isWaiting = false; + this.lastLockWaitedFor = this.lockWaitingFor; + this.lockWaitingFor = null; + mustCallPoolUpdateAtEnd = true; + break waitWithLock; + } + + // Wait + try { + + // -1 indicates to not use a timeout (this value is not later set to any other negative value) + long waitForNanos = -1; + if (timeoutTime != null) { + waitForNanos = Math.max(timeoutTime - System.nanoTime(), SERVER_THREAD_WAIT_NANOS_MINIMUM); + } else { + /* + Check if we should wait with a tick-based 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. + */ + if (this == MinecraftServer.serverThread) { + 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; + // Skip if the time is too short + if (waitForNanos >= SERVER_THREAD_WAIT_NANOS_MINIMUM) { + //noinspection ResultOfMethodCallIgnored + this.waitCondition.await(waitForNanos, TimeUnit.NANOSECONDS); + } + } else { + /* + 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. + */ + this.waitCondition.await(); + } + + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + + // Unmark this thread as waiting + this.isWaiting = false; + this.lastLockWaitedFor = this.lockWaitingFor; + this.lockWaitingFor = null; + mustCallPoolUpdateAtEnd = true; + + } finally { + this.waitLock.unlock(); + } + } + + } + + // Unregister this thread from the lock if necessary + if (registeredAsWaitingWithLock) { + yieldingLock.decrementWaitingThreads(); + } + + // Reset skipping the next wait + this.skipNextWait = false; + + // Update the pool if necessary + if (mustCallPoolUpdateAtEnd) { + BaseThreadActivation.callForUpdate(); + } + + } + + /** + * An auxiliary method for exclusive use in {@link #signal}, that marks the {@link YieldingLock} + * that this thread is waiting for as having been signalled for, so that no other threads + * are also signalled for it. + *
    + * This must be called when {@link #signal} returns true, and must be called before any other + * actions relating to the signalling of this thread are performed. + */ + private void markLockWaitingForAsSignalledFor() { + @Nullable YieldingLock lockWaitingFor = this.lockWaitingFor; + if (lockWaitingFor != null) { + lockWaitingFor.canBeSignalledFor = false; + } + } + + /** + * Signals this thread to wake up, or if it was not sleeping but attempting to poll a task: + * to not go to sleep the next time no task could be polled, and instead try polling a task again. + * + * @param reason The reason why this thread was signalled, or null if it is irrelevant (e.g. when the signal + * will never need to be repeated because there is only thread waiting for this specific event + * to happen). + * @return Whether this thread was sleeping before, and has woken up now, + * or whether {@link #skipNextWait} was set to true. + */ + @AnyThreadSafe + @YieldFree + public final boolean signal(@Nullable SignalReason reason) { + while (!this.waitLock.tryLock()) { // TODO Gale use a wait-free system here by using a sort of leave-a-message-at-the-door Atomic class system + Thread.onSpinWait(); + } + try { + if (this.isWaiting) { + if (this.isNotActuallyWaitingYet) { + if (!this.skipNextWait) { + this.markLockWaitingForAsSignalledFor(); + this.lastSignalReason = reason; + this.skipNextWait = true; + return true; + } + return false; + } + if (!this.mayBeStillWaitingButHasBeenSignalled) { + this.markLockWaitingForAsSignalledFor(); + this.lastSignalReason = reason; + this.mayBeStillWaitingButHasBeenSignalled = true; + this.waitCondition.signal(); + return true; + } + } else if (this.isPollingTaskOrCheckingStopCondition) { + if (!this.skipNextWait) { + this.markLockWaitingForAsSignalledFor(); + this.lastSignalReason = reason; + this.skipNextWait = true; + return true; + } + } + return false; + } finally { + this.waitLock.unlock(); + } + } + + /** + * @return The current thread if it is a {@link BaseThread}, or null otherwise. + */ + @SuppressWarnings("unused") + @AnyThreadSafe + @YieldFree + public static @Nullable BaseThread currentBaseThread() { + return Thread.currentThread() instanceof BaseThread baseThread ? baseThread : null; + } + + /** + * @return Whether the current thread is a {@link BaseThread}. + */ + @SuppressWarnings("unused") + @AnyThreadSafe + @YieldFree + public static boolean isBaseThread() { + return Thread.currentThread() instanceof BaseThread; + } + +} 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..ced372b40e8b3a5c43dabf5bb547a71e3c713d2f --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/OriginalServerThread.java @@ -0,0 +1,20 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.thread; + +import net.minecraft.server.MinecraftServer; +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/ServerThread.java b/src/main/java/org/galemc/gale/executor/thread/ServerThread.java new file mode 100644 index 0000000000000000000000000000000000000000..7d58d995d8e74cd5f51f85f123166bf884deed92 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/ServerThread.java @@ -0,0 +1,51 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.thread; + +import io.papermc.paper.util.TickThread; +import net.minecraft.server.MinecraftServer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.spigotmc.WatchdogThread; + +/** + * A {@link TickThread} that provides an implementation for {@link BaseThread}, + * that is shared between the {@link MinecraftServer#serverThread} and {@link WatchdogThread#instance}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public class ServerThread extends TickThread { + + protected ServerThread(final String name) { + super(name); + } + + protected ServerThread(final Runnable run, final String name) { + super(run, name); + } + + /** + * 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; + } + + /** + * @return The same value as {@link #getInstance()} if {@link MinecraftServer#isConstructed} is true, + * or null otherwise. + */ + public static @Nullable ServerThread getInstanceIfConstructed() { + return MinecraftServer.isConstructed ? getInstance() : null; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/SignalReason.java b/src/main/java/org/galemc/gale/executor/thread/SignalReason.java new file mode 100644 index 0000000000000000000000000000000000000000..436b0a8249290d833472da58ec01f9690be2fb95 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/SignalReason.java @@ -0,0 +1,23 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.thread; + +import org.galemc.gale.executor.lock.YieldingLock; + +/** + * A reason of a call to {@link SignallableThread#signal}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public enum SignalReason { + + /** + * A task that the signalled thread could poll and start is available. + */ + TASK, + /** + * The {@link YieldingLock} that the signalled thread was waiting for was released. + */ + YIELDING_LOCK + +} 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..a73aafc64dc60b57e2e5a91565e1aff612da6703 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/SignallableThread.java @@ -0,0 +1,31 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.thread; + +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.YieldFree; +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..8c4855c931ccc285768eabcff9d1f2b752d45bf6 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/deferral/ServerThreadDeferral.java @@ -0,0 +1,151 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.thread.deferral; + +import io.papermc.paper.util.TickThread; +import org.galemc.gale.concurrent.UnterminableExecutorService; +import org.galemc.gale.executor.TaskSpan; +import org.galemc.gale.executor.queue.BaseTaskQueues; +import org.galemc.gale.executor.thread.AbstractYieldingThread; +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.ServerThread; +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 BaseThread}, + * 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 + */ +@SuppressWarnings("unused") +public final class ServerThreadDeferral { + + private ServerThreadDeferral() {} + + /** + * @see #defer(Supplier, TaskSpan) + */ + public static void defer(Runnable task, TaskSpan span) { + deferInternal(task, null, span); + } + + /** + * 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 span The {@link TaskSpan} of the task. + */ + public static T defer(Supplier task, TaskSpan span) { + return deferInternal(null, task, span); + } + + /** + * Common implementation for {@link #defer(Runnable, TaskSpan)} and {@link #defer(Supplier, TaskSpan)}. + * Exactly one of {@code runnable} or {@code supplier} must be non-null. + */ + private static T deferInternal(@Nullable Runnable runnable, @Nullable Supplier supplier, TaskSpan span) { + // 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); + }, span); + yieldingThread.yieldUntil(null, 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); + } + }, span); + return future.join(); + } + } + + /** + * An executor for deferring {@link TaskSpan#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, TaskSpan.YIELDING); + } + + }; + + /** + * An executor for deferring {@link TaskSpan#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, TaskSpan.FREE); + } + + }; + + /** + * An executor for deferring {@link TaskSpan#TINY} tasks to the main thread, + * where {@link Executor#execute} calls {@link #defer}. + */ + public static final ExecutorService tinyExecutor = new UnterminableExecutorService() { + + @Override + public void execute(@NotNull Runnable runnable) { + defer(runnable, TaskSpan.TINY); + } + + }; + +} 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..77fe10e51b00115da520cfc211bf84badbc027be --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/deferral/TickThreadDeferral.java @@ -0,0 +1,159 @@ +// Gale - base thread pool + +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.TaskSpan; +import org.galemc.gale.executor.thread.AbstractYieldingThread; +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.queue.BaseTaskQueueTier; +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 BaseThread}, + * 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 + */ +@SuppressWarnings("unused") +public final class TickThreadDeferral { + + private TickThreadDeferral() {} + + /** + * This may be useful in the future. See the documentation of {@link BaseTaskQueueTier#SERVER}. + * + * @see #defer(Runnable, TaskSpan) + */ + public static void defer(final ServerLevel world, final int chunkX, final int chunkZ, Runnable task, TaskSpan span) { + defer(task, span); + } + + /** + * This may be useful in the future. See the documentation of {@link BaseTaskQueueTier#SERVER}. + * + * @see #defer(Supplier, TaskSpan) + */ + public static T defer(final ServerLevel world, final int chunkX, final int chunkZ, Supplier task, TaskSpan span) { + return defer(task, span); + } + + /** + * This may be useful in the future. See the documentation of {@link BaseTaskQueueTier#SERVER}. + * + * @see #defer(Runnable, TaskSpan) + */ + public static void defer(final Entity entity, Runnable task, TaskSpan span) { + defer(task, span); + } + + /** + * This may be useful in the future. See the documentation of {@link BaseTaskQueueTier#SERVER}. + * + * @see #defer(Supplier, TaskSpan) + */ + public static T defer(final Entity entity, Supplier task, TaskSpan span) { + return defer(task, span); + } + + /** + * @see #defer(Supplier, TaskSpan) + */ + public static void defer(Runnable task, TaskSpan span) { + // Current implementation uses ServerThreadDeferral + ServerThreadDeferral.defer(task, span); + } + + /** + * 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 span The {@link TaskSpan} of the task. + */ + public static T defer(Supplier task, TaskSpan span) { + // Current implementation uses ServerThreadDeferral + return ServerThreadDeferral.defer(task, span); + } + + /** + * An executor for deferring {@link TaskSpan#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, TaskSpan.YIELDING); + } + + }; + + /** + * An executor for deferring {@link TaskSpan#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, TaskSpan.FREE); + } + + }; + + /** + * An executor for deferring {@link TaskSpan#TINY} tasks to the main thread, + * where {@link Executor#execute} calls {@link #defer}. + */ + public static final ExecutorService tinyExecutor = new UnterminableExecutorService() { + + @Override + public void execute(@NotNull Runnable runnable) { + defer(runnable, TaskSpan.TINY); + } + + }; + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/pool/BaseThreadActivation.java b/src/main/java/org/galemc/gale/executor/thread/pool/BaseThreadActivation.java new file mode 100644 index 0000000000000000000000000000000000000000..65ad5020c5c5953c801fb6c31416e8658720e15f --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/pool/BaseThreadActivation.java @@ -0,0 +1,659 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.thread.pool; + +import net.minecraft.server.MinecraftServer; +import org.galemc.gale.executor.TaskSpan; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.lock.YieldingLock; +import org.galemc.gale.executor.queue.AbstractTaskQueue; +import org.galemc.gale.executor.queue.BaseTaskQueueTier; +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.ServerThread; +import org.galemc.gale.executor.thread.SignalReason; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +/** + * A class providing the static functionality needed to activate more threads in the {@link BaseThreadPool} + * when needed. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@AnyThreadSafe +@YieldFree +public final class BaseThreadActivation { + + private BaseThreadActivation() {} + + /** + * The delay in nanoseconds that is applied to {@link System#nanoTime()} + * when computing {@link #nextAllowedFrequentSignalNewTasksTime}. + */ + public static final long FREQUENT_SIGNAL_NEW_TASKS_INTERVAL = 100_000; + + /** + * The last time {@link #newTaskWasAdded}'s content was actually run. + * This value is useful to limit the number of runs of the method by potential frequent callers, + * such as the chunk task executors. + */ + private static final AtomicLong nextAllowedFrequentSignalNewTasksTime = new AtomicLong(System.nanoTime() - 1L); + + /** + * This value is not null while an update is ongoing. + * + * @see #update() + */ + static final AtomicReference updateOngoingOnThread = new AtomicReference(); + + /** + * Whether a non-{@link ServerThread} thread is ready to take over the {@link #update} call + * that is ongoing on a {@link ServerThread}. + * + * @see #callForUpdate() + */ + private static final AtomicBoolean isNonServerThreadReadyToTakeOverUpdate = new AtomicBoolean(); + + /** + * @see #update() + */ + private static final AtomicInteger newUpdateCallsReceived = new AtomicInteger(); + + /** + * A re-usable array for use inside {@link #update()}. + */ + private static final int[] numberOfThreadsActiveForTier = new int[BaseTaskQueueTier.length]; + + /** + * A re-usable array for use inside {@link #update()}. + */ + @SuppressWarnings("unchecked") + private static final List[] threadsWaitingForUnlockedLockForTier = new List[BaseTaskQueueTier.length]; + static { + for (int tierI = 0; tierI < BaseTaskQueueTier.length; tierI++) { + threadsWaitingForUnlockedLockForTier[tierI] = new ArrayList<>(); + } + } + + /** + * A re-usable array for use inside {@link #update()}. + */ + private static final int[] numberOfThreadsActiveForLowerThanTier = new int[BaseTaskQueueTier.length]; + + /** + * A re-usable array for use inside {@link #update()}. + */ + private static final int[] numberOfThreadsActiveForHigherThanTier = new int[BaseTaskQueueTier.length]; + + /** + * A re-usable array for use inside {@link #update()}. + */ + private static final int[] numberOfThreadsIntendedToBeActiveForTier = new int[BaseTaskQueueTier.length]; + + /** + * An array indicating, per {@link BaseTaskQueueTier} (indexed by their {@link BaseTaskQueueTier#ordinal}) + * per {@link TaskSpan} (indexed by their {@link TaskSpan#ordinal}) per whether for queues that allow being + * yielded to ({@link AbstractTaskQueue#canBeYieldedTo()}) (1) or not (0), whether there may be tasks + * for that tier and span, indicated by whether the value is positive (indicating true) or 0 (indicating false). + * It is always incremented before calling {@link #update()} due to new tasks being added. + * If it is 0, it is certain that either there are no queued task for the tier, or + * a task has just been added to the queue and this value has not yet been set to true, but will be due + * to a {@link #newTaskWasAdded} call, which is then followed by a {@link #callForUpdate()} call. + */ + public static final AtomicInteger[][][] thereMayBeTasks = new AtomicInteger[BaseTaskQueueTier.length][TaskSpan.length][2]; + static { + for (int tierI = 0; tierI < BaseTaskQueueTier.length; tierI++) { + for (int spanI = 0; spanI < TaskSpan.length; spanI++) { + for (int canBeYieldedTo = 0; canBeYieldedTo <= 1; canBeYieldedTo++) { + thereMayBeTasks[tierI][spanI][canBeYieldedTo] = new AtomicInteger(); + } + } + } + } + + /** + * The {@link BaseTaskQueueTier#ordinal} of the highest tier (which means the lowest + * {@link BaseTaskQueueTier#ordinal}) for which the number of present threads + * have been determined by the last call to {@link #update()} to be in excess. This value is + * {@link BaseTaskQueueTier#length} when no threads are in excess. + */ + public static volatile int tierInExcessOrdinal = BaseTaskQueueTier.length; + + private static long updateNextAllowedFrequentSignalNewTasksTime(long value) { + long newValue = System.nanoTime() + FREQUENT_SIGNAL_NEW_TASKS_INTERVAL; + return newValue - value >= 0 ? newValue : value; + } + + /** + * @see #newTaskWasAdded(BaseTaskQueueTier, TaskSpan, boolean, boolean) + */ + public static void newTaskWasAdded(BaseTaskQueueTier tier, TaskSpan span) { + newTaskWasAdded(tier, span, true, false); + } + + /** + * This method is to be called when a new task has become available to be polled. + * The task must already have been added to the data structure that a thread would poll from, + * in a way that is visible to any thread (for example by adding it to a concurrent data structure). + * Otherwise, the resulting attempt at activating threads would not be able to observe these new tasks yet. + *
    + * When a task is added that is not important enough to warrant doing a full {@link #update}, + * calling this method may be skipped. + *
    + * Additionally, this method may be called when no new task has been added, but there is a suspicion of new tasks + * existing for which no {@link #update} was called. A concrete example of this is when a thread is activated + * due to tasks it can poll being available, but then upon activation, acquiring a {@link YieldingLock} it was + * waiting for instead. + */ + public static void newTaskWasAdded(BaseTaskQueueTier tier, TaskSpan span, boolean canBeYieldedTo, boolean onlyIfLastTimeIsTooLongAgo) { + + if (thereMayBeTasks[tier.ordinal][span.ordinal][canBeYieldedTo ? 1 : 0].getAndIncrement() == 0) { + // Always call update() if we just set the thereMayBeTasks value to true + onlyIfLastTimeIsTooLongAgo = false; + } + + // Check and update nextAllowedFrequentSignalNewTasksTime + if (!onlyIfLastTimeIsTooLongAgo || System.nanoTime() - nextAllowedFrequentSignalNewTasksTime.get() >= 0) { + nextAllowedFrequentSignalNewTasksTime.updateAndGet(BaseThreadActivation::updateNextAllowedFrequentSignalNewTasksTime); + // Update + callForUpdate(); + } else { + // Do not start an update, but do increment the received calls + newUpdateCallsReceived.incrementAndGet(); + } + + } + + /** + * This method is to be called when a {@link YieldingLock} has been released. + * The lock must already have been unlocked. Otherwise, the resulting attempt at activating + * threads would not be able to observe the lock being released yet. + */ + public static void yieldingLockWithWaitingThreadsWasUnlocked() { + // Update + callForUpdate(); + } + + /** + * Either starts an {@link #update()}, or lets another thread that is already doing an update know + * that it will have to do another one. + *
    + * Only one thread can be performing an update at a time. + * If a second thread calls this method while an update is ongoing + * (signified by {@link #updateOngoingOnThread} being non-null), + * the thread performing an update will perform another update after finishing the current one, due to the + * second thread incrementing {@link #newUpdateCallsReceived}. + *
    + * After a thread property (or another property that is used in a similar way) + * that is used within {@link #update()} is changed, this method must be called. + * This currently equates to the following values: + *
      + *
    • {@link BaseThread#highestTierOfTaskOnStack}
    • + *
    • + * {@link BaseThread#isWaiting} and {@link BaseThread#lockWaitingFor}, + * which are always updated in tandem, and {@link BaseThread#isNotActuallyWaitingYet} and + * {@link BaseThread#skipNextWait}, which are set at similar times as {@link BaseThread#isWaiting}. + *
    • + *
    • + * {@link BaseThread#canStartYieldingTasks} and the values + * {@link BaseThread#yieldDepth} and {@link BaseThread#heldYieldingLockCount} it depends on. + * //TODO Gale We currently do not call callForUpdate just due to changes in heldYieldingLockCount, do we really have to? That would cause a lot of calls. + *
    • + *
    + * This specifically does not include: + *
      + *
    • + * The following values that are only used + * in the meta-handling of {@link #update()}, not in the activation of threads: + *
        + *
      • {@link #newUpdateCallsReceived}
      • + *
      • {@link #updateOngoingOnThread}
      • + *
      + *
    • + *
    • + * The following values that are never changed outside of {@link #update()}: + *
        + *
      • {@link #numberOfThreadsActiveForTier}
      • + *
      • {@link #threadsWaitingForUnlockedLockForTier}
      • + *
      • {@link #numberOfThreadsActiveForLowerThanTier}
      • + *
      • {@link #numberOfThreadsActiveForHigherThanTier}
      • + *
      • {@link #numberOfThreadsIntendedToBeActiveForTier}
      • + *
      + *
    • + *
    • + * {@link #thereMayBeTasks}, which is only set to 0 outside of {@link #update()} + * (specifically, in {@link BaseThread}), which will only prevent the {@link #update()} call from + * exploring the existence of tasks for a specific {@link BaseTaskQueueTier} and {@link TaskSpan} when + * there are in fact no such tasks, thereby not causing any reason to do another update. + *
    • + *
    + */ + public static void callForUpdate() { + Thread currentThread = Thread.currentThread(); + // Make sure the updating thread repeats (must be set before evaluating isUpdateOngoing) + newUpdateCallsReceived.incrementAndGet(); + // Start the update ourselves if necessary + boolean amIServerThread = currentThread instanceof ServerThread; + boolean amIDoingUpdate = false; + // Start the update if not ongoing + if (updateOngoingOnThread.get() == null && updateOngoingOnThread.compareAndSet(null, currentThread)) { + amIDoingUpdate = true; + } else if (!amIServerThread) { + // Take over the update from the server thread if necessary + Thread updatePerformingThread = updateOngoingOnThread.get(); + if (updatePerformingThread instanceof ServerThread) { + // Make sure we are the only thread ready to taking over from the server thread + if (!isNonServerThreadReadyToTakeOverUpdate.get() && !isNonServerThreadReadyToTakeOverUpdate.getAndSet(true)) { + // Busy wait until the server thread has stopped updating + while (updateOngoingOnThread.get() instanceof ServerThread) { + Thread.onSpinWait(); + } + // Start the update, if another thread did not already quickly claim it in the meantime + if (updateOngoingOnThread.compareAndSet(null, currentThread)) { + amIDoingUpdate = true; + } + isNonServerThreadReadyToTakeOverUpdate.set(false); + } + } + } + + if (amIDoingUpdate) { + // Perform an update + do { + try { + /* + * If newUpdateCallsReceived is zero here, it was set to 0 between the check for whether + * it is positive and the setting to true of isUpdateGoing in the while statement below, + * or it was set to 0 between the increment and the subsequent setting to true of isUpdateGoing + * at the start of this method. + */ + if (newUpdateCallsReceived.get() > 0) { + update(); + } + } finally { + // Take actions to let another thread take over the update + boolean isBeingTakenOver = amIServerThread && isNonServerThreadReadyToTakeOverUpdate.get(); + if (isBeingTakenOver) { + // Make sure an iteration is performed + newUpdateCallsReceived.incrementAndGet(); + } + updateOngoingOnThread.set(null); + if (isBeingTakenOver) { + // Skip the loop checks + break; + } + } + /* + If newUpdateCallsReceived is positive here, it was increased between it being set to 0 and + updateOngoingOnThread being set to null, so we must repeat. + */ + } while (newUpdateCallsReceived.get() > 0 && updateOngoingOnThread.get() == null && updateOngoingOnThread.compareAndSet(null, currentThread)); + } + } + + /** + * Determines whether it could be useful to activate the given thread. + * This does not into account whether tasks {@linkplain #thereMayBeTasks may exist} at all, + * except for the checking on non-yielding {@link BaseTaskQueueTier#SERVER} tasks for the purpose explained below. + *
    + * We only activate threads that can start yielding tasks + * (it seems wasteful to take the effort to activate threads that can not), + * or threads that are waiting for a lock that is not currently locked. + *
    + * Note that for the server thread, if it cannot start yielding tasks, + * there is never an alternative thread that can, so we also allow it, + * as a special case, to be activated purely based on the existence of non-yielding tasks. + *
    + * This method must only be called from {@link #update}. + * + * @param thread The thread to consider. + * @param lockWaitingFor The pre-computed value of {@link BaseThread#lockWaitingFor}. + * @param isServerThread Whether the given thread is a {@link ServerThread}. + * @return Whether it could be useful to activate the given thread. + */ + private static boolean couldBeUsefullyActivatedForTasksOrLock(BaseThread thread, @Nullable YieldingLock lockWaitingFor, boolean isServerThread) { + if (!thread.isWaitingAndNeedsSignal()) { + // There is no point in activating the thread because it is not waiting + return false; + } + if (lockWaitingFor != null && !lockWaitingFor.isLocked() && lockWaitingFor.canBeSignalledFor) { + // Activating the thread would be useful because there is a lock that can be acquired + return true; + } + if (thread.canStartYieldingTasks) { + // Activating the thread would be useful because it can start yielding tasks + return true; + } + if (isServerThread) { + // The server thread can be activated whenever there are any non-yielding tasks + for (TaskSpan span : TaskSpan.NON_YIELDING_VALUES) { + if (thereMayBeTasks[BaseTaskQueueTier.SERVER.ordinal][span.ordinal][0].get() + thereMayBeTasks[BaseTaskQueueTier.SERVER.ordinal][span.ordinal][1].get() > 0) { + return true; + } + } + } + /* + There is no point in activating this thread (for anything that the thread could do, + it would be better to activate a different or newly instantiated thread). + */ + return false; + } + + /** + * Activates threads as necessary, and computes whether threads must de-activate themselves when they can. + *
    + * This method is called from {@link #callForUpdate()} if necessary. + */ + static void update() { + MinecraftServer.THREAD_DEBUG_LOGGER.ifPresent(it -> it.info("update")); + boolean amIServerThread = Thread.currentThread() instanceof ServerThread; + boolean madeChangesInLastIteration = false; + int numberOfUpdateCallsAtStartOfLastIteration = -1; + boolean isFirstIteration = true; + /* + Keep updating while necessary (while marked to repeat by another call, + or while this update itself made some change in the previous iteration, + to be sure we only stop when we found no more changes to make). + */ + updateWhileNecessary: + while (true) { + MinecraftServer.THREAD_DEBUG_LOGGER.ifPresent(it -> it.info("iteration of update")); + + // Let a non-server thread take over if needed + if (amIServerThread && isNonServerThreadReadyToTakeOverUpdate.get()) { + // All preparations for the take-over are performed in #callForUpdate + break; + } + + // Break the loop if needed + if (isFirstIteration) { + // Always run an iteration if this is the first one + isFirstIteration = false; + numberOfUpdateCallsAtStartOfLastIteration = newUpdateCallsReceived.decrementAndGet(); + } else { + if (madeChangesInLastIteration) { + /* + If we made changes in the last iteration, + we can quit only if no more update calls have been received at all. + */ + int oldNewUpdateCallsReceived = newUpdateCallsReceived.getAndUpdate(value -> value == 0 ? 0 : value - 1); + if (oldNewUpdateCallsReceived == 0) { + break; + } + numberOfUpdateCallsAtStartOfLastIteration = oldNewUpdateCallsReceived - 1; + } else { + /* + If we made no changes in the last iteration, + we can quit if no update calls were received in the meantime. + In that case, we can reset newUpdateCallsReceived as we have finished all necessary updates. + */ + final int finalNumberOfUpdateCallsAtStartOfLastIteration = numberOfUpdateCallsAtStartOfLastIteration; + int oldNewUpdateCallsReceived = newUpdateCallsReceived.getAndUpdate(value -> value == finalNumberOfUpdateCallsAtStartOfLastIteration ? 0 : value - 1); + if (oldNewUpdateCallsReceived == numberOfUpdateCallsAtStartOfLastIteration) { + break; + } + numberOfUpdateCallsAtStartOfLastIteration = oldNewUpdateCallsReceived - 1; + } + } + + // Reset madeChangesInLastIteration + madeChangesInLastIteration = false; + + // Get the threads + @Nullable BaseThread @NotNull [] threads = BaseThreadPool.getBaseThreads(); + + /* + Compute for each tier, for how many threads + the highest tier of any task on their stack is that tier, + and that are not waiting. + Additionally, compute the threads for each tier that are waiting for some YieldingLock + (threads with no tasks on their stack cannot be waiting for a YieldingLock) + that can be unlocked. + Additionally, compute the number of threads that are active (i.e. not waiting) + but that are not executing a task (i.e. do not have any tasks on their stack). + */ + Arrays.fill(numberOfThreadsActiveForTier, 0); + for (int tierI = 0; tierI < BaseTaskQueueTier.length; tierI++) { + threadsWaitingForUnlockedLockForTier[tierI].clear(); + } + int activeAssistThreadsWithoutTask = 0; + for (BaseThread thread : threads) { + if (thread != null) { + BaseTaskQueueTier tier = thread.highestTierOfTaskOnStack; + // This value will be unused if tier is null, so we can safely use some dummy value like 1 + int tierOrdinal = thread.baseThreadIndex > 0 ? (tier == null ? -1 : tier.ordinal) : BaseTaskQueueTier.SERVER.ordinal; + if (thread.isWaitingAndNeedsSignal()) { + var lockWaitingFor = thread.lockWaitingFor; + if (lockWaitingFor != null && !lockWaitingFor.isLocked() && lockWaitingFor.canBeSignalledFor) { + threadsWaitingForUnlockedLockForTier[tierOrdinal].add(thread); + } + } else { + if (tier == null && thread.baseThreadIndex > 0) { + /* + ^ Note that assist threads are never waiting for a YieldingLock while they are doing nothing, + so we can safely use 'else' below, but the server thread may be waiting for a YieldingLock + while it has no highestTierOfTaskOnStack, because it reached a YieldingLock during its + normal outer execution. + */ + /* + The thread is doing nothing: + if it is also not waiting, it is available to start anything. + */ + activeAssistThreadsWithoutTask++; + } else { + numberOfThreadsActiveForTier[tierOrdinal]++; + } + } + } else { + numberOfThreadsActiveForTier[BaseTaskQueueTier.SERVER.ordinal]++; + } + } + + /* + Compute the exclusive cumulative value of numberOfThreadsActiveForTier from above, + as being for how many threads the highest tier of any task on their stack + is a strictly lower priority tier. + */ + System.arraycopy(numberOfThreadsActiveForTier, 1, numberOfThreadsActiveForLowerThanTier, 0, BaseTaskQueueTier.length - 1); + for (int tierI = BaseTaskQueueTier.length - 2; tierI >= 0; tierI--) { + numberOfThreadsActiveForLowerThanTier[tierI] += numberOfThreadsActiveForLowerThanTier[tierI + 1]; + } + + /* + Compute the exclusive cumulative value of numberOfThreadsActiveForTier from below, + as being for how many threads the highest tier of any task on their stack + is a strictly higher priority tier. + */ + System.arraycopy(numberOfThreadsActiveForTier, 0, numberOfThreadsActiveForHigherThanTier, 1, BaseTaskQueueTier.length - 1); + for (int tierI = 2; tierI < BaseTaskQueueTier.length; tierI++) { + numberOfThreadsActiveForHigherThanTier[tierI] += numberOfThreadsActiveForHigherThanTier[tierI - 1]; + } + + /* + For each tier, compute the number of threads that should be active if there were tasks. + This can then later be compared to the actual number of active threads for that tier. + */ + for (int tierI = 1; tierI < BaseTaskQueueTier.length; tierI++) { + numberOfThreadsIntendedToBeActiveForTier[tierI] = BaseThreadPool.targetParallelism - activeAssistThreadsWithoutTask - numberOfThreadsActiveForHigherThanTier[tierI] - Math.min(numberOfThreadsActiveForLowerThanTier[tierI], BaseThreadPool.maxUndisturbedLowerTierThreadCount); + } + /* + There must always be an attempt to have one active server thread. + The above computation would sometimes give 0 due to not wanting to disturb lower tiers, + which we don't care about for the server thread because it is always more important. + The above computation would also sometimes give a number higher than 1 which makes no sense. + */ + numberOfThreadsIntendedToBeActiveForTier[0] = 1; + + { + final int finalActiveAssistThreadsWithoutTask = activeAssistThreadsWithoutTask; + MinecraftServer.THREAD_DEBUG_LOGGER.ifPresent(it -> it.info("Target parallelism = " + BaseThreadPool.targetParallelism + ", active threads without task = " + finalActiveAssistThreadsWithoutTask + ", active threads for tiers = " + Arrays.toString(numberOfThreadsActiveForTier) + ", number of threads intended to be active for tiers = " + Arrays.toString(numberOfThreadsIntendedToBeActiveForTier))); + } + + /* + * Determine the highest tier for which the number of threads that are active exceeds + * the number of threads that should be active if there were tasks. + * If none, set tierInExcessOrdinal to BaseTaskQueueTier#length. + */ + for (int tierI = 0;; tierI++) { + if (tierI == BaseTaskQueueTier.length || numberOfThreadsActiveForTier[tierI] > numberOfThreadsIntendedToBeActiveForTier[tierI]) { + tierInExcessOrdinal = tierI; + break; + } + } + + /* + Try to activate a thread, for higher to lower priority tier tasks, in order: + if a thread is activated, we continue with another update iteration, so that we make a + good-as-possible attempt to activate threads for higher priority tier tasks first. + */ + for (int tierI = 0; tierI < BaseTaskQueueTier.length; tierI++) { + // Only if we need to activate threads + if (numberOfThreadsActiveForTier[tierI] < numberOfThreadsIntendedToBeActiveForTier[tierI]) { + /* + Only if there may be tasks at all (which, if true, will be the reason provided when signalling a + thread), or if there is a thread already at this exact tier that is waiting for a YieldingLock. + */ + boolean thereAreTasks = false; + boolean thereAreOnlyTasksThatCanNotBeYieldedTo = false; + for (int spanI = 0; spanI < TaskSpan.length; spanI++) { + if (thereMayBeTasks[tierI][spanI][1].get() > 0) { + thereAreTasks = true; + break; + } else if (thereMayBeTasks[tierI][spanI][0].get() > 0) { + thereAreTasks = true; + thereAreOnlyTasksThatCanNotBeYieldedTo = true; + break; + } + } + if (thereAreTasks || !threadsWaitingForUnlockedLockForTier[tierI].isEmpty()) { + + /* + * We attempt to wake up a thread that is sleeping, + * or add a new thread to start running. + * Of course, we can only choose a thread that could poll a task. + * We only choose a thread that can accept yielding tasks, even if + * the added task is yield-free, so that we have a lower chance of + * the chosen thread getting stuck again quickly. + * Out of the possible threads, we attempt to choose one that is waiting for a YieldingLock + * that is available, so that we have a thread owning this lock as quickly as possible again, + * making the next time it is released again sooner as well. + * Out of the possible threads that are not waiting for a lock, + * we attempt to choose one with non-zero yield depth over any with zero yield depth, + * since we must later wake up this thread anyway. Then, we attempt to choose one with the + * lowest possible yield depth, so that it can still keep yielding as much as possible. + */ + /* + Special case: only the server thread can start SERVER tasks, + and we never activate it for other tiers, because it could only start tiny tasks. + */ + int tryThreadsStart, tryThreadsEnd; + if (tierI == 0) { + tryThreadsStart = 0; + tryThreadsEnd = 1; + } else { + tryThreadsStart = 1; + tryThreadsEnd = threads.length; + } + while (true) { + // Find the best thread to activate + int threadIToUpdate = -1; + boolean threadIToUpdateIsWaitingForAvailableYieldingLock = false; + int threadIToUpdateYieldDepth = 0; + int threadIToUpdateYieldPotential = 0; + int threadIToUpdateTierOrdinalOrLength = 0; + for (int threadI = tryThreadsStart; threadI < tryThreadsEnd; threadI++) { + BaseThread thread = threads[threadI]; + if (thread != null) { + @Nullable YieldingLock lockWaitingFor = thread.lockWaitingFor; + if (couldBeUsefullyActivatedForTasksOrLock(thread, lockWaitingFor, tierI == 0)) { + /* + Tasks of a certain tier may yield to tasks of the same or a higher + tier, and they may also yield to tiny tasks of a lower tier. + We do not want to wake up a thread just for tiny tasks + unless it has zero yield depth, + so we only activate threads that have either no tasks on their stack, + or only tasks of the same or a lower tier, where a lower priority tier + is preferred (but not as important as the yield depth). + Of course, this only takes into account tasks, and we may also + activate threads due to them waiting on an available YieldingLock. + */ + var highestTierOfTaskOnStack = thread.highestTierOfTaskOnStack; + var highestTierOfTaskOnStackOrdinalOrLength = highestTierOfTaskOnStack == null ? BaseTaskQueueTier.length : highestTierOfTaskOnStack.ordinal; + boolean isThreadWaitingForAvailableYieldingLock = lockWaitingFor != null && !lockWaitingFor.isLocked() && lockWaitingFor.canBeSignalledFor; + if (isThreadWaitingForAvailableYieldingLock || highestTierOfTaskOnStack == null || highestTierOfTaskOnStack.ordinal >= tierI) { + int yieldDepth = thread.yieldDepth; + if (!thereAreOnlyTasksThatCanNotBeYieldedTo || yieldDepth == 0) { + boolean isBestChoice = false; + int yieldPotential = thread.maximumYieldDepth - yieldDepth; + if (threadIToUpdate == -1) { + isBestChoice = true; + } else if (isThreadWaitingForAvailableYieldingLock != threadIToUpdateIsWaitingForAvailableYieldingLock) { + isBestChoice = isThreadWaitingForAvailableYieldingLock; + } else if (threadIToUpdateYieldDepth == 0 && yieldDepth != 0) { + isBestChoice = true; + } else if (yieldDepth != 0) { + if (yieldPotential > threadIToUpdateYieldPotential) { + isBestChoice = true; + } else if (highestTierOfTaskOnStackOrdinalOrLength > threadIToUpdateTierOrdinalOrLength) { + isBestChoice = true; + } + } + if (isBestChoice) { + threadIToUpdate = threadI; + threadIToUpdateIsWaitingForAvailableYieldingLock = isThreadWaitingForAvailableYieldingLock; + threadIToUpdateYieldDepth = yieldDepth; + threadIToUpdateYieldPotential = yieldPotential; + threadIToUpdateTierOrdinalOrLength = highestTierOfTaskOnStackOrdinalOrLength; + } + } + } + } + } + } + if (threadIToUpdate == -1) { + // No valid thread was found + break; + } + // Check if the thread still seems valid and attempt to activate it + BaseThread thread = threads[threadIToUpdate]; + @Nullable YieldingLock lockWaitingFor = thread.lockWaitingFor; + if (couldBeUsefullyActivatedForTasksOrLock(thread, lockWaitingFor, tierI == 0)) { + // Wake up the thread + if (thread.signal(thereAreTasks ? SignalReason.TASK : SignalReason.YIELDING_LOCK)) { + // Make sure no other threads are activated for the same unlocking event + // Do another update + madeChangesInLastIteration = true; + continue updateWhileNecessary; + } + } + /* + The thread was not valid to activate anymore, or not activated, + so we attempt to find a valid thread again. + */ + } + + // Because no thread was activated, we add one (only if we were looking for an AssistThread) + if (tierI != 0) { + BaseThreadPool.addAssistThread(); + // Do another update + madeChangesInLastIteration = true; + continue updateWhileNecessary; + } + + } + } + } + + } + } + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/pool/BaseThreadPool.java b/src/main/java/org/galemc/gale/executor/thread/pool/BaseThreadPool.java new file mode 100644 index 0000000000000000000000000000000000000000..36c9faaf2d431dbc5a173a3821752725b497dd6c --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/pool/BaseThreadPool.java @@ -0,0 +1,219 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.thread.pool; + +import net.minecraft.server.MinecraftServer; +import org.galemc.gale.executor.queue.BaseTaskQueueTier; +import org.galemc.gale.executor.thread.AssistThread; +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.ServerThread; +import org.galemc.gale.util.CPUCoresEstimation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; + +/** + * A pool of threads that can perform tasks to assist the current {@link ServerThread}. These tasks can be of + * different {@linkplain BaseTaskQueueTier tiers}. + *
    + * This pool intends to keep {@link #targetParallelism} threads active at any time, + * which includes a potentially active {@link ServerThread}. + *
    + * As such, this pool is closely intertwined with the {@link ServerThread}. This pool can not control the + * {@link ServerThread} in any way, but it is responsible for signalling the {@link ServerThread} when tasks become + * available in a {@link BaseTaskQueueTier#SERVER} task queue, and for listening for when the {@link ServerThread} + * becomes (in)active in order to update the number of active {@link AssistThread}s accordingly. + *

    + * Updates to the threads in this pool are done in a lock-free manner that attempts to do the right thing with + * the volatile information that is available. In some cases, this may cause a thread to be woken up when it + * should not have been, and so on, but the updates being lock-free is more significant than the updates being + * optimal in a high-contention environment. The environment is not expected to have high enough contention for + * this to have much of an impact. Additionally, the suboptimalities in updates are always optimistic in terms of + * making/keeping threads active rather than inactive, and can not a situation where a thread was intended + * to be active, but ends but not being active. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public final class BaseThreadPool { + + private BaseThreadPool() {} + + public static final String targetParallelismEnvironmentVariable = "gale.threads.target"; + public static final String maxUndisturbedLowerTierThreadCountEnvironmentVariable = "gale.threads.undisturbed"; + + /** + * The target number of threads that will be actively in use by this pool, + * which includes a potentially active {@link ServerThread}. + *
    + * This value is always positive. + *
    + * 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. + *
    + * The computed value above can be overridden using the {@link #targetParallelismEnvironmentVariable}. + */ + public static final int targetParallelism; + static { + int parallelismByEnvironmentVariable = Integer.getInteger(targetParallelismEnvironmentVariable, -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; + } + targetParallelism = Math.max(1, targetParallelismBeforeSetAtLeastOne); + } + + /** + * The maximum number of threads to be executing tasks, that only have tasks on their thread that are strictly + * below a certain tier, before a thread wishing to execute such tasks gets activated regardless. + * If this threshold of lower tier threads is not exceeded, activating a thread to execute a higher tier task + * will be delayed until one of the active threads finishes execution of their stack or blocks for another + * reason. + *
    + * This value is always nonnegative. + *
    + * This value is currently automatically determined according to the following rule: + *
      + *
    • 0, if {@link #targetParallelism} = 1
    • + *
    • {@code max(1, floor(2/5 * }{@link #targetParallelism}{@code ))}
    • + *
    + * The computed value above can be overridden using the {@link #maxUndisturbedLowerTierThreadCountEnvironmentVariable}. + */ + public static final int maxUndisturbedLowerTierThreadCount; + static { + int maxUndisturbedLowerTierThreadCountByEnvironmentVariable = Integer.getInteger(maxUndisturbedLowerTierThreadCountEnvironmentVariable, -1); + maxUndisturbedLowerTierThreadCount = maxUndisturbedLowerTierThreadCountByEnvironmentVariable >= 0 ? maxUndisturbedLowerTierThreadCountByEnvironmentVariable : targetParallelism == 1 ? 0 : Math.max(1, targetParallelism * 2 / 5); + } + + /** + * An array of the {@link AssistThread}s in this pool, indexed by their {@link AssistThread#assistThreadIndex}. + *
    + * This field must only ever be changed from within {@link #addAssistThread}. + */ + private static volatile AssistThread[] assistThreads = new AssistThread[0]; + + /** + * An array of the {@link BaseThread}s in this pool, indexed by their {@link BaseThread#baseThreadIndex}. + *
    + * This field must not be referenced anywhere outside {@link #addAssistThread} or {@link #getBaseThreads()}: + * it only holds the last computed value. + */ + private static volatile @Nullable BaseThread @NotNull [] lastComputedBaseThreads = new BaseThread[1]; + + /** + * Creates a new {@link AssistThread}, adds it to this pool and starts it. + *
    + * Must only be called from within {@link BaseThreadActivation#update()} while + * {@link BaseThreadActivation#isUpdateOngoing} is true. + */ + public static void addAssistThread() { + int oldThreadsLength = assistThreads.length; + int newThreadsLength = oldThreadsLength + 1; + // Expand the thread array + AssistThread[] newAssistThreads = Arrays.copyOf(assistThreads, newThreadsLength); + // Create the new thread + AssistThread newThread = newAssistThreads[oldThreadsLength] = new AssistThread(oldThreadsLength); + // Save the new thread array + assistThreads = newAssistThreads; + // Update the assist threads in baseThreads + @SuppressWarnings("NonAtomicOperationOnVolatileField") + BaseThread[] newLastComputedBaseThreads = lastComputedBaseThreads = Arrays.copyOf(lastComputedBaseThreads, newThreadsLength + 1); + newLastComputedBaseThreads[newThreadsLength] = newThread; + // Start the thread + newThread.start(); + MinecraftServer.THREAD_DEBUG_LOGGER.ifPresent(it -> it.info("Added assist thread " + newAssistThreads.length)); + } + + /** + * The {@link BaseThread}s ({@link ServerThread}s and {@link AssistThread}s) in this thread pool, + * specifically for the purpose of easy iteration. + *
    + * Note that the {@link ServerThread} at index 0 may be null if {@link MinecraftServer#isConstructed} is false. + *
    + * Must only be called from within {@link BaseThreadActivation#update()} while + * {@link BaseThreadActivation#isUpdateOngoing} is true. + */ + static @Nullable BaseThread @NotNull [] getBaseThreads() { + // Store in a non-local volatile + @Nullable BaseThread @NotNull [] baseThreads = lastComputedBaseThreads; + // Update the server thread if necessary + baseThreads[0] = ServerThread.getInstanceIfConstructed(); + // Return the value + return baseThreads; + } + + /** + * This method must not be called with {@code index} = 0 while {@link MinecraftServer#isConstructed} is false. + * + * @return The {@link BaseThread} with the given {@link BaseThread#baseThreadIndex}. + * This must not be called + */ + public static @NotNull BaseThread getThreadByBaseIndex(int index) { + if (index == 0) { + return ServerThread.getInstance(); + } + return assistThreads[index - 1]; + } + + /** + * @return The same value as {@link #getThreadByBaseIndex} if {@link MinecraftServer#isConstructed} is true + * or if the given {@code index} is not 0, + * or null otherwise (i.e. if {@link MinecraftServer#isConstructed} is false and the given {@code index} is 0). + */ + @SuppressWarnings("unused") + public static @Nullable BaseThread getThreadByBaseIndexIfConstructed(int index) { + return index != 0 || MinecraftServer.isConstructed ? getThreadByBaseIndex(index) : null; + } + + public static AssistThread getThreadByAssistIndex(int index) { + return assistThreads[index]; + } + +} diff --git a/src/main/java/org/spigotmc/SpigotCommand.java b/src/main/java/org/spigotmc/SpigotCommand.java index 3112a8695639c402e9d18710acbc11cff5611e9c..7b38565b8699bd083c2114981feb2202321b8486 100644 --- a/src/main/java/org/spigotmc/SpigotCommand.java +++ b/src/main/java/org/spigotmc/SpigotCommand.java @@ -31,7 +31,7 @@ public class SpigotCommand extends Command { MinecraftServer console = MinecraftServer.getServer(); org.spigotmc.SpigotConfig.init((File) console.options.valueOf("spigot-settings")); - for (ServerLevel world : console.getAllLevels()) { + for (ServerLevel world : console.getAllLevelsArray()) { // Gale - base thread pool - optimize server levels world.spigotConfig.init(); } console.server.reloadCount++; diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java index 832f1ee4fb11c981bd109510eb908d7c7ef91bd4..bcb144ec4a836b8b32f60726bcbee218a4f62742 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.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 pool { public static final boolean DISABLE_WATCHDOG = Boolean.getBoolean("disable.watchdog"); // Paper - private static WatchdogThread instance; + public static WatchdogThread instance; // Gale - base thread pool - private -> public private long timeoutTime; private boolean restart; private final long earlyWarningEvery; // Paper - Timeout time for just printing a dump but not restarting @@ -206,7 +206,7 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Gale!):" ); // Paper // Gale - branding changes io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(isLongTimeout); // Paper // Paper - rewrite chunk system this.dumpTickingInfo(); // Paper - log detailed tick information - WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log ); + WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.serverThread.getId(), Integer.MAX_VALUE ), log ); // Gale - base thread pool log.log( Level.SEVERE, "------------------------------" ); // // Paper start - Only print full dump on long timeouts