9
0
mirror of https://github.com/Dreeam-qwq/Gale.git synced 2025-12-22 16:29:26 +00:00
Files
Gale/patches/server/0127-Base-thread-pools.patch
2022-12-24 21:59:56 +01:00

6829 lines
313 KiB
Diff

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Martijn Muijsers <martijnmuijsers@live.nl>
Date: Fri, 2 Dec 2022 11:43:51 +0100
Subject: [PATCH] Base thread pools
License: AGPL-3.0 (https://www.gnu.org/licenses/agpl-3.0.html)
Gale - https://galemc.org
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java
index b71404be2c82f7db35272b367af861e94d6c73d3..238e7aa6e8a9e9f26bc6dee8d7e49a853c3cc0e2 100644
--- a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java
+++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java
@@ -1,10 +1,15 @@
package ca.spottedleaf.concurrentutil.executor.standard;
+import net.minecraft.server.MinecraftServer;
+import org.galemc.gale.executor.queue.BaseTaskQueues;
+
import java.util.ArrayDeque;
import java.util.concurrent.atomic.AtomicLong;
public class PrioritisedThreadedTaskQueue implements PrioritisedExecutor {
+ private final boolean influenceMayHaveDelayedTasks; // Gale - base thread pools
+
protected final ArrayDeque<PrioritisedTask>[] queues = new ArrayDeque[Priority.TOTAL_SCHEDULABLE_PRIORITIES]; {
for (int i = 0; i < Priority.TOTAL_SCHEDULABLE_PRIORITIES; ++i) {
this.queues[i] = new ArrayDeque<>();
@@ -20,6 +25,16 @@ public class PrioritisedThreadedTaskQueue implements PrioritisedExecutor {
protected long taskIdGenerator = 0;
+ public PrioritisedThreadedTaskQueue() {
+ this(false);
+ }
+
+ // Gale start - base thread pools
+ public PrioritisedThreadedTaskQueue(boolean influenceMayHaveDelayedTasks) {
+ this.influenceMayHaveDelayedTasks = influenceMayHaveDelayedTasks;
+ }
+ // Gale end - base thread pools
+
@Override
public PrioritisedExecutor.PrioritisedTask queueRunnable(final Runnable task, final PrioritisedExecutor.Priority priority) throws IllegalStateException, IllegalArgumentException {
if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
@@ -145,6 +160,12 @@ public class PrioritisedThreadedTaskQueue implements PrioritisedExecutor {
}
protected final long getAndAddTotalScheduledTasksVolatile(final long value) {
+ // Gale start - base thread pools
+ if (this.influenceMayHaveDelayedTasks) {
+ MinecraftServer.nextTimeAssumeWeMayHaveDelayedTasks = true;
+ BaseTaskQueues.allLevelsScheduledTickThreadChunk.signalReason.signalAnother();
+ }
+ // Gale end - base thread pools
return this.totalScheduledTasks.getAndAdd(value);
}
@@ -158,6 +179,12 @@ public class PrioritisedThreadedTaskQueue implements PrioritisedExecutor {
return this.totalCompletedTasks.getAndAdd(value);
}
+ // Gale start - base thread pools
+ public final boolean hasScheduledUncompletedTasksVolatile() {
+ return this.totalScheduledTasks.get() > this.totalCompletedTasks.get();
+ }
+ // Gale end - base thread pools
+
protected static final class PrioritisedTask implements PrioritisedExecutor.PrioritisedTask {
protected final PrioritisedThreadedTaskQueue queue;
protected long id;
diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java
index 4f3670b2bdb8b1b252e9f074a6af56a018a8c465..757229093ecce162c99e27c2f92ead2e1a1a2b10 100644
--- a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java
+++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java
@@ -22,6 +22,7 @@ import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.*;
import org.bukkit.Bukkit;
+import org.galemc.gale.executor.queue.ScheduledServerThreadTaskQueues;
import java.util.*;
import java.util.concurrent.Executor;
@@ -181,7 +182,7 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo
if (!Bukkit.isPrimaryThread()) {
// Plugins?
- MinecraftServer.getServer().scheduleOnMain(() -> modifyBlocks(chunkPacket, chunkPacketInfo));
+ ScheduledServerThreadTaskQueues.add(() -> modifyBlocks(chunkPacket, chunkPacketInfo), ScheduledServerThreadTaskQueues.ANTI_XRAY_MODIFY_BLOCKS_TASK_MAX_DELAY); // Gale - base thread pools
return;
}
diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java
index 84cc9397237fa0c17aa1012dfb5683c90eb6d3b8..3686b0330b48119e08cbc1528cfd86c5ec7bd8e9 100644
--- a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java
+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java
@@ -113,7 +113,7 @@ public final class ChunkTaskScheduler {
public final PrioritisedThreadPool.PrioritisedPoolExecutor parallelGenExecutor;
public final PrioritisedThreadPool.PrioritisedPoolExecutor loadExecutor;
- private final PrioritisedThreadedTaskQueue mainThreadExecutor = new PrioritisedThreadedTaskQueue();
+ public final PrioritisedThreadedTaskQueue mainThreadExecutor = new PrioritisedThreadedTaskQueue(true); // Gale end - base thread pools - private -> public, count delayed tasks
final ReentrantLock schedulingLock = new ReentrantLock();
public final ChunkHolderManager chunkHolderManager;
diff --git a/src/main/java/io/papermc/paper/configuration/Configurations.java b/src/main/java/io/papermc/paper/configuration/Configurations.java
index cf6d50218769e3fecd12dbde70a03b5042feddf4..9b94defa9520ac5c17c9b8bf1cfb3b17ddac2d22 100644
--- a/src/main/java/io/papermc/paper/configuration/Configurations.java
+++ b/src/main/java/io/papermc/paper/configuration/Configurations.java
@@ -322,7 +322,7 @@ public abstract class Configurations<G, W> {
YamlConfiguration global = YamlConfiguration.loadConfiguration(this.globalFolder.resolve(this.globalConfigFileName).toFile());
ConfigurationSection worlds = global.createSection(legacyWorldsSectionKey);
worlds.set(legacyWorldDefaultsSectionKey, YamlConfiguration.loadConfiguration(this.globalFolder.resolve(this.defaultWorldConfigFileName).toFile()));
- for (ServerLevel level : server.getAllLevels()) {
+ for (ServerLevel level : server.getAllLevelsArray()) { // Gale - base thread pools - optimize server levels
worlds.set(level.getWorld().getName(), YamlConfiguration.loadConfiguration(getWorldConfigFile(level).toFile()));
}
return global;
diff --git a/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java b/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java
index 3088d5f008a8cb5a75f1e11bd80a2614a4c1b75d..0c771f024cec0b3fbb68e4eeeeb778187a89456f 100644
--- a/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java
+++ b/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java
@@ -285,7 +285,7 @@ public class PaperConfigurations extends Configurations<GlobalConfiguration, Wor
try {
this.initializeGlobalConfiguration(reloader(this.globalConfigClass, GlobalConfiguration.get()));
this.initializeWorldDefaultsConfiguration();
- for (ServerLevel level : server.getAllLevels()) {
+ for (ServerLevel level : server.getAllLevelsArray()) { // Gale - base thread pools - optimize server levels
this.createWorldConfig(createWorldContextMap(level), reloader(this.worldConfigClass, level.paperConfig()));
}
} catch (Exception ex) {
diff --git a/src/main/java/io/papermc/paper/util/MCUtil.java b/src/main/java/io/papermc/paper/util/MCUtil.java
index 3fad7e58a1461d897526d63efd27075f044f2962..79b375c921f9d94b269bac4af9637569bc75c1af 100644
--- a/src/main/java/io/papermc/paper/util/MCUtil.java
+++ b/src/main/java/io/papermc/paper/util/MCUtil.java
@@ -2,12 +2,10 @@ package io.papermc.paper.util;
import com.destroystokyo.paper.profile.CraftPlayerProfile;
import com.destroystokyo.paper.profile.PlayerProfile;
-import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.internal.Streams;
import com.google.gson.stream.JsonWriter;
-import com.mojang.datafixers.util.Either;
import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet;
import java.lang.ref.Cleaner;
import it.unimi.dsi.fastutil.objects.ReferenceArrayList;
@@ -17,16 +15,12 @@ import net.minecraft.server.MinecraftServer;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ChunkHolder;
-import net.minecraft.server.level.ChunkMap;
-import net.minecraft.server.level.DistanceManager;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
-import net.minecraft.server.level.Ticket;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.ClipContext;
import net.minecraft.world.level.Level;
-import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkStatus;
import org.apache.commons.lang.exception.ExceptionUtils;
import com.mojang.authlib.GameProfile;
@@ -34,7 +28,7 @@ import org.bukkit.Location;
import org.bukkit.block.BlockFace;
import org.bukkit.craftbukkit.CraftWorld;
import org.bukkit.craftbukkit.util.Waitable;
-import org.spigotmc.AsyncCatcher;
+import org.galemc.gale.executor.queue.BaseTaskQueues;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -42,11 +36,9 @@ import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Queue;
-import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -55,22 +47,8 @@ import java.util.function.Consumer;
import java.util.function.Supplier;
public final class MCUtil {
- public static final ThreadPoolExecutor asyncExecutor = new ThreadPoolExecutor(
- 0, 2, 60L, TimeUnit.SECONDS,
- new LinkedBlockingQueue<>(),
- new ThreadFactoryBuilder()
- .setNameFormat("Paper Async Task Handler Thread - %1$d")
- .setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(MinecraftServer.LOGGER))
- .build()
- );
- public static final ThreadPoolExecutor cleanerExecutor = new ThreadPoolExecutor(
- 1, 1, 0L, TimeUnit.SECONDS,
- new LinkedBlockingQueue<>(),
- new ThreadFactoryBuilder()
- .setNameFormat("Paper Object Cleaner")
- .setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(MinecraftServer.LOGGER))
- .build()
- );
+ public static final Executor asyncExecutor = BaseTaskQueues.scheduledAsync.yieldingExecutor; // Gale - base thread pools - remove Paper async executor
+ public static final Executor cleanerExecutor = BaseTaskQueues.cleaner.executor; // Gale - base thread pools - remove Paper cleaner executor
public static final long INVALID_CHUNK_KEY = getCoordinateKey(Integer.MAX_VALUE, Integer.MAX_VALUE);
diff --git a/src/main/java/io/papermc/paper/util/TickThread.java b/src/main/java/io/papermc/paper/util/TickThread.java
index fc57850b80303fcade89ca95794f63910404a407..88abb082a994d5dbc3b51e770acfbd4f0a7af1f9 100644
--- a/src/main/java/io/papermc/paper/util/TickThread.java
+++ b/src/main/java/io/papermc/paper/util/TickThread.java
@@ -3,10 +3,14 @@ package io.papermc.paper.util;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
-import org.bukkit.Bukkit;
+import org.galemc.gale.executor.queue.AbstractTaskQueue;
+import org.galemc.gale.executor.queue.BaseTaskQueues;
+import org.galemc.gale.executor.thread.BaseYieldingThread;
+import org.galemc.gale.executor.thread.pooled.ServerThreadPool;
+
import java.util.concurrent.atomic.AtomicInteger;
-public class TickThread extends Thread {
+public abstract class TickThread extends BaseYieldingThread { // Gale - base thread pools
public static final boolean STRICT_THREAD_CHECKS = Boolean.getBoolean("paper.strict-thread-checks");
@@ -65,7 +69,7 @@ public class TickThread extends Thread {
}
private TickThread(final Runnable run, final String name, final int id) {
- super(run, name);
+ super(run, name, ServerThreadPool.taskQueues, ServerThreadPool.threadsWaitingForYieldingTasks, ServerThreadPool.threadsWaitingForFreeTasks, true, 1); // Gale - base thread pools
this.id = id;
}
diff --git a/src/main/java/io/papermc/paper/world/ThreadedWorldUpgrader.java b/src/main/java/io/papermc/paper/world/ThreadedWorldUpgrader.java
index 95cac7edae8ac64811fc6a2f6b97dd4a0fceb0b0..e0220511648861ef9363f260814da69d216f781d 100644
--- a/src/main/java/io/papermc/paper/world/ThreadedWorldUpgrader.java
+++ b/src/main/java/io/papermc/paper/world/ThreadedWorldUpgrader.java
@@ -7,25 +7,21 @@ import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceKey;
import net.minecraft.util.worldupdate.WorldUpgrader;
import net.minecraft.world.level.ChunkPos;
-import net.minecraft.world.level.Level;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.storage.ChunkStorage;
import net.minecraft.world.level.chunk.storage.RegionFileStorage;
-import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.dimension.LevelStem;
-import net.minecraft.world.level.levelgen.WorldGenSettings;
import net.minecraft.world.level.storage.DimensionDataStorage;
import net.minecraft.world.level.storage.LevelStorageSource;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import org.galemc.gale.executor.queue.BaseTaskQueues;
+
import java.io.File;
import java.io.IOException;
import java.text.DecimalFormat;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;
@@ -46,6 +42,10 @@ public class ThreadedWorldUpgrader {
this.dimensionType = dimensionType;
this.worldName = worldName;
this.worldDir = worldDir;
+ // Gale start - base thread pools - remove world upgrade executors
+ this.threadPool = BaseTaskQueues.scheduledAsync.yieldingExecutor;
+ /*
+ // Gale end - base thread pools - remove world upgrade executors
this.threadPool = Executors.newFixedThreadPool(Math.max(1, threads), new ThreadFactory() {
private final AtomicInteger threadCounter = new AtomicInteger();
@@ -61,6 +61,7 @@ public class ThreadedWorldUpgrader {
return ret;
}
});
+ */ // Gale - base thread pools - remove world upgrade executors
this.dataFixer = dataFixer;
this.generatorKey = generatorKey;
this.removeCaches = removeCaches;
diff --git a/src/main/java/me/titaniumtown/ArrayConstants.java b/src/main/java/me/titaniumtown/ArrayConstants.java
index ceec23a85aae625fbbe2db95c8e9c83fb9f9767c..2a7f2a410a0622d8c4bf45533ecb549a73df153e 100644
--- a/src/main/java/me/titaniumtown/ArrayConstants.java
+++ b/src/main/java/me/titaniumtown/ArrayConstants.java
@@ -2,6 +2,8 @@
package me.titaniumtown;
+import net.minecraft.server.level.ServerLevel;
+
public final class ArrayConstants {
private ArrayConstants() {}
@@ -14,5 +16,6 @@ public final class ArrayConstants {
public static final long[] emptyLongArray = new long[0];
public static final org.bukkit.entity.Entity[] emptyBukkitEntityArray = new org.bukkit.entity.Entity[0];
public static final net.minecraft.world.entity.Entity[] emptyEntityArray = new net.minecraft.world.entity.Entity[0];
+ public static final ServerLevel[] emptyServerLevelArray = new ServerLevel[0]; // Gale - base thread pools
}
diff --git a/src/main/java/net/minecraft/Util.java b/src/main/java/net/minecraft/Util.java
index 5ef58831a857fd8aa4ac30147762dc17d773a53e..352ed1db3399bc9453286e28c84f61b6325de78b 100644
--- a/src/main/java/net/minecraft/Util.java
+++ b/src/main/java/net/minecraft/Util.java
@@ -26,9 +26,6 @@ import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.spi.FileSystemProvider;
-import java.security.AccessController;
-import java.security.PrivilegedActionException;
-import java.security.PrivilegedExceptionAction;
import java.time.Duration;
import java.time.Instant;
import java.time.ZonedDateTime;
@@ -47,8 +44,6 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
-import java.util.concurrent.ForkJoinPool;
-import java.util.concurrent.ForkJoinWorkerThread;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@@ -67,11 +62,11 @@ import java.util.stream.Stream;
import javax.annotation.Nullable;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.Bootstrap;
-import net.minecraft.util.Mth;
import net.minecraft.util.RandomSource;
import net.minecraft.util.TimeSource;
import net.minecraft.util.datafix.DataFixers;
import net.minecraft.world.level.block.state.properties.Property;
+import org.galemc.gale.executor.queue.BaseTaskQueues;
import org.slf4j.Logger;
public class Util {
@@ -79,8 +74,8 @@ public class Util {
private static final int DEFAULT_MAX_THREADS = 255;
private static final String MAX_THREADS_SYSTEM_PROPERTY = "max.bg.threads";
private static final AtomicInteger WORKER_COUNT = new AtomicInteger(1);
- private static final ExecutorService BOOTSTRAP_EXECUTOR = makeExecutor("Bootstrap", -2); // Paper - add -2 priority
- private static final ExecutorService BACKGROUND_EXECUTOR = makeExecutor("Main", -1); // Paper - add -1 priority
+ private static final ExecutorService BACKGROUND_EXECUTOR = BaseTaskQueues.scheduledAsync.yieldingExecutor; // Gale - base thread pools - remove background executor
+ private static final ExecutorService BOOTSTRAP_EXECUTOR = BACKGROUND_EXECUTOR; // Gale - Patina - remove bootstrap executor
// Paper start - don't submit BLOCKING PROFILE LOOKUPS to the world gen thread
public static final ExecutorService PROFILE_EXECUTOR = Executors.newFixedThreadPool(2, new java.util.concurrent.ThreadFactory() {
@@ -219,7 +214,6 @@ public class Util {
}
public static void shutdownExecutors() {
- shutdownExecutor(BACKGROUND_EXECUTOR);
shutdownExecutor(IO_POOL);
}
diff --git a/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java b/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java
index f25b9330e068c7d9e12cb57a7761cfef9ebaf7bc..c20f793c4792b5f06a90d305671ac6d473253644 100644
--- a/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java
+++ b/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java
@@ -152,10 +152,7 @@ public class EntitySelector {
if (this.isWorldLimited()) {
this.addEntities(list, source.getLevel(), vec3d, predicate);
} else {
- Iterator iterator1 = source.getServer().getAllLevels().iterator();
-
- while (iterator1.hasNext()) {
- ServerLevel worldserver1 = (ServerLevel) iterator1.next();
+ for (ServerLevel worldserver1 : source.getServer().getAllLevelsArray()) { // Gale - base thread pools - optimize server levels
this.addEntities(list, worldserver1, vec3d, predicate);
}
diff --git a/src/main/java/net/minecraft/network/protocol/PacketUtils.java b/src/main/java/net/minecraft/network/protocol/PacketUtils.java
index 27d4aa45e585842c04491839826d405d6f447f0e..f36a0e29f2ba464eee3fabba0b98a714a3b8a49d 100644
--- a/src/main/java/net/minecraft/network/protocol/PacketUtils.java
+++ b/src/main/java/net/minecraft/network/protocol/PacketUtils.java
@@ -2,6 +2,7 @@ package net.minecraft.network.protocol;
import com.mojang.logging.LogUtils;
import net.minecraft.network.PacketListener;
+import org.galemc.gale.executor.AbstractBlockableEventLoop;
import org.slf4j.Logger;
// CraftBukkit start
@@ -9,7 +10,6 @@ import net.minecraft.server.MinecraftServer;
import net.minecraft.server.RunningOnDifferentThreadException;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.network.ServerGamePacketListenerImpl;
-import net.minecraft.util.thread.BlockableEventLoop;
public class PacketUtils {
@@ -36,10 +36,10 @@ public class PacketUtils {
public PacketUtils() {}
public static <T extends PacketListener> void ensureRunningOnSameThread(Packet<T> packet, T listener, ServerLevel world) throws RunningOnDifferentThreadException {
- PacketUtils.ensureRunningOnSameThread(packet, listener, (BlockableEventLoop) world.getServer());
+ PacketUtils.ensureRunningOnSameThread(packet, listener, world.getServer()); // Gale - base thread pools
}
- public static <T extends PacketListener> void ensureRunningOnSameThread(Packet<T> packet, T listener, BlockableEventLoop<?> engine) throws RunningOnDifferentThreadException {
+ public static <T extends PacketListener> void ensureRunningOnSameThread(Packet<T> packet, T listener, AbstractBlockableEventLoop engine) throws RunningOnDifferentThreadException { // Gale - base thread pools
if (!engine.isSameThread()) {
engine.execute(() -> { // Paper - Fix preemptive player kick on a server shutdown.
packetProcessing.push(listener); // Paper - detailed watchdog information
diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java
index 0c4c62674b4c7e8e3921c7eb3ef726759ac75075..31b488a3c1b81b99bf5bda9f90c3cf697d727841 100644
--- a/src/main/java/net/minecraft/server/Main.java
+++ b/src/main/java/net/minecraft/server/Main.java
@@ -1,27 +1,22 @@
package net.minecraft.server;
-import com.mojang.authlib.GameProfile;
-import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService;
import com.mojang.datafixers.DataFixer;
import com.mojang.datafixers.util.Pair;
import com.mojang.logging.LogUtils;
import com.mojang.serialization.DynamicOps;
-import com.mojang.serialization.Lifecycle;
+
import java.awt.GraphicsEnvironment;
import java.io.File;
import java.net.Proxy;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
-import java.util.UUID;
import java.util.function.BooleanSupplier;
+
+import com.mojang.serialization.Lifecycle;
import io.papermc.paper.world.ThreadedWorldUpgrader;
-import joptsimple.NonOptionArgumentSpec;
-import joptsimple.OptionParser;
import joptsimple.OptionSet;
-import joptsimple.OptionSpec;
import net.minecraft.CrashReport;
-import net.minecraft.DefaultUncaughtExceptionHandler;
import net.minecraft.SharedConstants;
import net.minecraft.Util;
import net.minecraft.commands.Commands;
@@ -57,6 +52,8 @@ import net.minecraft.world.level.storage.LevelStorageSource;
import net.minecraft.world.level.storage.LevelSummary;
import net.minecraft.world.level.storage.PrimaryLevelData;
import net.minecraft.world.level.storage.WorldData;
+import org.galemc.gale.executor.queue.BaseTaskQueues;
+import org.galemc.gale.executor.thread.pooled.AsyncThreadPool;
import org.slf4j.Logger;
// CraftBukkit start
@@ -64,7 +61,7 @@ import com.google.common.base.Charsets;
import com.mojang.bridge.game.PackType;
import java.io.InputStreamReader;
import java.util.concurrent.atomic.AtomicReference;
-import net.minecraft.SharedConstants;
+
import org.bukkit.configuration.file.YamlConfiguration;
// CraftBukkit end
@@ -228,6 +225,15 @@ public class Main {
WorldStem worldstem;
+ // Gale start - base thread pools
+ // Initialize the task queues by calling an arbitrary method on the last queue
+ //noinspection ResultOfMethodCallIgnored
+ BaseTaskQueues.scheduledAsync.hashCode();
+ // Initialize the async executor by calling an arbitrary method
+ //noinspection ResultOfMethodCallIgnored
+ AsyncThreadPool.instance.hashCode();
+ // Gale end - base thread pools
+
try {
WorldLoader.InitConfig worldloader_c = Main.loadOrCreateConfig(dedicatedserversettings.getProperties(), convertable_conversionsession, flag, resourcepackrepository);
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
index eb951c9fda85d9620d3038a3db22d578db45e878..60ed76588347f4d4c09d8df4952bf55501ed7c00 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -40,7 +40,6 @@ import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
-import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
@@ -120,7 +119,6 @@ import net.minecraft.util.profiling.metrics.profiling.InactiveMetricsRecorder;
import net.minecraft.util.profiling.metrics.profiling.MetricsRecorder;
import net.minecraft.util.profiling.metrics.profiling.ServerMetricsSamplersProvider;
import net.minecraft.util.profiling.metrics.storage.MetricsPersister;
-import net.minecraft.util.thread.ReentrantBlockableEventLoop;
import net.minecraft.world.Difficulty;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.ai.village.VillageSiege;
@@ -161,7 +159,15 @@ import net.minecraft.world.level.storage.loot.PredicateManager;
import net.minecraft.world.phys.Vec2;
import net.minecraft.world.phys.Vec3;
import org.apache.commons.lang3.Validate;
+import org.galemc.gale.executor.MinecraftServerBlockableEventLoop;
import org.galemc.gale.configuration.GaleConfigurations;
+import org.galemc.gale.executor.annotation.thread.OriginalServerThreadOnly;
+import org.galemc.gale.executor.queue.BaseTaskQueues;
+import org.galemc.gale.executor.queue.ScheduledServerThreadTaskQueues;
+import org.galemc.gale.executor.thread.OriginalServerThread;
+import org.galemc.gale.executor.thread.pooled.AsyncThreadPool;
+import org.galemc.gale.executor.thread.wait.SignalReason;
+import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
// CraftBukkit start
@@ -192,11 +198,19 @@ import org.bukkit.event.server.ServerLoadEvent;
import co.aikar.timings.MinecraftTimings; // Paper
-public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTask> implements CommandSource, AutoCloseable {
+public abstract class MinecraftServer extends MinecraftServerBlockableEventLoop implements CommandSource, AutoCloseable { // Gale - base thread pools
public static final int SERVER_THREAD_PRIORITY = Integer.getInteger("gale.thread.priority.server", 8); // Gale - server thread priority environment variable
- private static MinecraftServer SERVER; // Paper
+ // Gale start - base thread pools
+ public static MinecraftServer SERVER; // Paper // Gale - base thread pools - private -> public
+
+ /**
+ * Whether {@link #SERVER} has been set.
+ */
+ public static boolean isConstructed;
+
+ // Gale end - base thread pools
public static final Logger LOGGER = LogUtils.getLogger();
public static final String VANILLA_BRAND = "vanilla";
private static final float AVERAGE_TICK_TIME_SMOOTHING = 0.8F;
@@ -226,6 +240,10 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
private int port;
private final LayeredRegistryAccess<RegistryLayer> registries;
private Map<ResourceKey<Level>, ServerLevel> levels;
+ // Gale start - base thread pools - optimize server levels
+ private @NotNull ServerLevel @NotNull [] levelArray = ArrayConstants.emptyServerLevelArray;
+ private @Nullable ServerLevel overworld;
+ // Gale end - base thread pools - optimize server levels
private PlayerList playerList;
private volatile boolean running;
private volatile boolean isRestarting = false; // Paper - flag to signify we're attempting to restart
@@ -255,10 +273,115 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
private long lastOverloadWarning;
protected final Services services;
private long lastServerStatus;
- public final Thread serverThread;
- private long nextTickTime;
- private long delayedTasksMaxNextTickTime;
- private boolean mayHaveDelayedTasks;
+ public static OriginalServerThread serverThread; // Gale - base thread pools - rename, instance -> static, final -> non-final (but still effectively final)
+ // Gale start - base thread pools - make fields volatile
+ private volatile long nextTickTime;
+ private volatile long delayedTasksMaxNextTickTime;
+ // Gale end - base thread pools - make fields volatile
+
+ // Gale start - base thread pools
+
+ public static volatile long nextTickStartNanoTime;
+ public static volatile long delayedTasksMaxNextTickNanoTime;
+
+ /**
+ * Sets {@link #nextTickTime}, and sets {@link #nextTickStartNanoTime} accordingly.
+ */
+ private void setNextTickTime(long nextTickTime) {
+ this.nextTickTime = nextTickTime;
+ /*
+ Add 10000 nanoseconds, to make sure the currentTime() >= nextTickTime check will be true after this moment
+ regardless of the nanosecond granularity of the Condition#await function, which is probably somewhere around
+ 26 nanoseconds.
+ */
+ nextTickStartNanoTime = 1_000_000L * this.nextTickTime + 10_000L;
+ }
+
+ /**
+ * Sets {@link #delayedTasksMaxNextTickTime}, and sets {@link #delayedTasksMaxNextTickNanoTime} accordingly.
+ *
+ * @see #setNextTickTime
+ */
+ private void setDelayedTasksMaxNextTickTime(long delayedTasksMaxNextTickTime) {
+ this.delayedTasksMaxNextTickTime = delayedTasksMaxNextTickTime;
+ delayedTasksMaxNextTickNanoTime = 1_000_000L * this.delayedTasksMaxNextTickTime + 10_000L;
+ }
+
+ /**
+ * Whether to skip the next call to {@link #mayHaveDelayedTasks()} and simply return true.
+ * This is typically set to true when a new task is added to a queue with tasks that count as potentially
+ * delayed tasks, or when an element from such a queue is successfully polled (even though it may afterwards be
+ * empty, it seems better to simply poll again next time, rather than perform the full {@link #mayHaveDelayedTasks()}
+ * check that loops over all queues).
+ */
+ public static volatile boolean nextTimeAssumeWeMayHaveDelayedTasks;
+
+ /**
+ * Whether the value of {@link #lastComputedMayHaveDelayedTasks} should be assumed to be correct.
+ */
+ public static volatile boolean mayHaveDelayedTasksIsCurrentlyComputed;
+
+ /**
+ * The cached last computed correct (except for potential race condition mistakes in the computation)
+ * value of {@link #mayHaveDelayedTasks()}.
+ */
+ public static volatile boolean lastComputedMayHaveDelayedTasks;
+
+ /**
+ * Whether the server is currently in spare time after a tick.
+ * This is set to true by the {@link #serverThread} when entering the spare time phase,
+ * either at the end of a tick, or at the start of one (if it occurred too early), and set to false after
+ * the corresponding {@link #managedBlock} call.
+ */
+ public static volatile boolean isInSpareTime = false;
+
+ /**
+ * Whether the server is currently waiting for the next tick, which is one of the cases where
+ * {@link #isInSpareTime} is true. Specifically, the other case where {@link #isInSpareTime} is true is
+ * while {@link #isOversleep} is true.
+ */
+ public static volatile boolean isWaitingUntilNextTick = false;
+
+ /**
+ * A potentially out-of-date value indicating whether {@link #isInSpareTime} is true
+ * and {@link #haveTime()} is false and {@link #blockingCount} is 0.
+ * This should be updated just in time before it is potentially needed.
+ */
+ public static volatile boolean isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking = false;
+
+ /**
+ * The stop condition provided to the current call of {@link #managedBlock}, or null if no {@link #managedBlock}
+ * call is ongoing.
+ */
+ public static volatile @Nullable BooleanSupplier currentManagedBlockStopCondition;
+
+ /**
+ * Whether the {@link #currentManagedBlockStopCondition} has become true
+ * during the last {@link #managedBlock} call.
+ */
+ public static volatile boolean currentManagedBlockStopConditionHasBecomeTrue = false;
+
+ public static final SignalReason managedBlockStopConditionBecameTrueSignalReason = SignalReason.createNonRetrying();
+
+ public static void signalServerThreadIfCurrentManagedBlockStopConditionBecameTrue() {
+ if (currentManagedBlockStopConditionHasBecomeTrue) {
+ // We already signalled the thread
+ return;
+ }
+ if (currentManagedBlockStopCondition == null) {
+ // There is no ongoing managedBlock cal
+ return;
+ }
+ if (!currentManagedBlockStopCondition.getAsBoolean()) {
+ // The stop condition is not true
+ return;
+ }
+ currentManagedBlockStopConditionHasBecomeTrue = true;
+ serverThread.signal(managedBlockStopConditionBecameTrueSignalReason);
+ }
+
+ // Gale start - base thread pools
+
private final PackRepository packRepository;
private final ServerScoreboard scoreboard;
@Nullable
@@ -287,7 +410,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
public java.util.Queue<Runnable> processQueue = new java.util.concurrent.ConcurrentLinkedQueue<Runnable>();
public int autosavePeriod;
public Commands vanillaCommandDispatcher;
- public boolean forceTicks; // Paper
+ public volatile boolean forceTicks; // Paper // Gale - base thread pools - make fields volatile
// CraftBukkit end
// Spigot start
public static final int TPS = 20;
@@ -303,9 +426,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
public volatile boolean abnormalExit = false; // Paper
public boolean isIteratingOverLevels = false; // Paper
- public static <S extends MinecraftServer> S spin(Function<Thread, S> serverFactory) {
+ public static <S extends MinecraftServer> S spin(Function<OriginalServerThread, S> serverFactory) { // Gale - base thread pools
AtomicReference<S> atomicreference = new AtomicReference();
- Thread thread = new io.papermc.paper.util.TickThread(() -> { // Paper - rewrite chunk system
+ OriginalServerThread thread = new OriginalServerThread(() -> { // Paper - rewrite chunk system // Gale - base thread pools
((MinecraftServer) atomicreference.get()).runServer();
}, "Server thread");
@@ -324,16 +447,19 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
return s0;
}
- public MinecraftServer(OptionSet options, WorldLoader.DataLoadContext worldLoader, Thread thread, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PackRepository resourcepackrepository, WorldStem worldstem, Proxy proxy, DataFixer datafixer, Services services, ChunkProgressListenerFactory worldloadlistenerfactory) {
- super("Server");
+ // Gale start - base thread pools
+ public MinecraftServer(OptionSet options, WorldLoader.DataLoadContext worldLoader, OriginalServerThread thread, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PackRepository resourcepackrepository, WorldStem worldstem, Proxy proxy, DataFixer datafixer, Services services, ChunkProgressListenerFactory worldloadlistenerfactory) {
+ super();
+ // Gale end - base thread pools
SERVER = this; // Paper - better singleton
+ isConstructed = true; // Gale - base thread pools
this.status = new ServerStatus();
this.random = RandomSource.create();
this.port = -1;
this.levels = Maps.newLinkedHashMap();
this.running = true;
this.tickTimes = new long[100];
- this.nextTickTime = Util.getMillis();
+ this.setNextTickTime(Util.getMillis()); // Gale - base thread pools
this.scoreboard = new ServerScoreboard(this);
this.customBossEvents = new CustomBossEvents();
this.frameTimer = new FrameTimer();
@@ -359,7 +485,10 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
HolderGetter<Block> holdergetter = this.registries.compositeAccess().registryOrThrow(Registries.BLOCK).asLookup().filterFeatures(this.worldData.enabledFeatures());
this.structureTemplateManager = new StructureTemplateManager(worldstem.resourceManager(), convertable_conversionsession, datafixer, holdergetter);
- this.serverThread = thread;
+ // Gale start - base thread pools
+ serverThread = thread;
+ AsyncThreadPool.instance.intendedActiveThreadCountMayHaveChanged();
+ // Gale end - base thread pools
this.executor = Util.backgroundExecutor();
}
// CraftBukkit start
@@ -599,7 +728,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
}
this.forceDifficulty();
- for (ServerLevel worldserver : this.getAllLevels()) {
+ for (ServerLevel worldserver : this.getAllLevelsArray()) { // Gale - base thread pools - optimize server levels
this.prepareLevels(worldserver.getChunkSource().chunkMap.progressListener, worldserver);
//worldserver.entityManager.tick(); // SPIGOT-6526: Load pending entities so they are available to the API // Paper - rewrite chunk system, not required to "tick" anything
this.server.getPluginManager().callEvent(new org.bukkit.event.world.WorldLoadEvent(worldserver.getWorld()));
@@ -758,7 +887,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
//ChunkProviderServer chunkproviderserver = worldserver.getChunkProvider(); // Paper - move up
chunkproviderserver.getLightEngine().setTaskPerBatch(500);
- this.nextTickTime = Util.getMillis();
+ this.setNextTickTime(Util.getMillis()); // Gale - base thread pools
// Paper start - configurable spawn reason
int radiusBlocks = worldserver.paperConfig().spawn.keepSpawnLoadedRange * 16;
int radiusChunks = radiusBlocks / 16 + ((radiusBlocks & 15) != 0 ? 1 : 0);
@@ -802,6 +931,11 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
worldserver.setSpawnSettings(worldserver.serverLevelData.getDifficulty() != Difficulty.PEACEFUL && ((DedicatedServer) this).settings.getProperties().spawnMonsters, this.isSpawningAnimals()); // Paper - per level difficulty (from setDifficulty(ServerLevel, Difficulty, boolean))
this.forceTicks = false;
+ // Gale start - base thread pools
+ if (isWaitingUntilNextTick) {
+ signalServerThreadIfCurrentManagedBlockStopConditionBecameTrue();
+ }
+ // Gale end - base thread pools
// CraftBukkit end
}
@@ -828,8 +962,12 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
// Paper end - rewrite chunk system - add close param
boolean flag3 = false;
- for (Iterator iterator = this.getAllLevels().iterator(); iterator.hasNext(); flag3 = true) {
- ServerLevel worldserver = (ServerLevel) iterator.next();
+ // Gale start - base thread pools - optimize server levels
+ ServerLevel[] worldservers = this.getAllLevelsArray();
+ for (int worldserverI = 0; worldserverI < worldservers.length; flag3 = true) {
+ ServerLevel worldserver = worldservers[worldserverI];
+ worldserverI++;
+ // Gale end - base thread pools - optimize server levels
if (!suppressLogs) {
MinecraftServer.LOGGER.info("Saving chunks for level '{}'/{}", worldserver, worldserver.dimension().location());
@@ -853,14 +991,6 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
*/
// CraftBukkit end
if (flush) {
- Iterator iterator1 = this.getAllLevels().iterator();
-
- while (iterator1.hasNext()) {
- ServerLevel worldserver2 = (ServerLevel) iterator1.next();
-
- //MinecraftServer.LOGGER.info("ThreadedAnvilChunkStorage ({}): All chunks are saved", worldserver2.getChunkSource().chunkMap.getStorageName()); // Paper - move up
- }
-
MinecraftServer.LOGGER.info("ThreadedAnvilChunkStorage: All dimensions are saved");
}
@@ -887,7 +1017,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
// CraftBukkit start
- private boolean hasStopped = false;
+ public boolean hasStopped = false; // Gale - base thread pools - private -> public
public volatile boolean hasFullyShutdown = false; // Paper
private boolean hasLoggedStop = false; // Paper
private final Object stopLock = new Object();
@@ -916,8 +1046,10 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
*/
MinecraftServer.LOGGER.info("Stopping main thread (Ignore any thread death message you see! - DO NOT REPORT THREAD DEATH TO PAPER - If you think this is a Gale bug, please report it at https://github.com/GaleMC/Gale/issues )");
// Gale end - branding changes
- while (this.getRunningThread().isAlive()) {
- this.getRunningThread().stop();
+ // Gale start - base thread pools
+ while (serverThread.isAlive()) {
+ serverThread.stop();
+ // Gale end - base thread pools
try {
Thread.sleep(1);
} catch (InterruptedException e) {}
@@ -948,12 +1080,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
MinecraftServer.LOGGER.info("Saving worlds");
- Iterator iterator = this.getAllLevels().iterator();
-
- ServerLevel worldserver;
-
- while (iterator.hasNext()) {
- worldserver = (ServerLevel) iterator.next();
+ for (ServerLevel worldserver : this.getAllLevelsArray()) { // Gale - base thread pools - optimize server levels
if (worldserver != null) {
worldserver.noSave = false;
}
@@ -970,9 +1097,6 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
MinecraftServer.LOGGER.error("Failed to unlock level {}", this.storageSource.getLevelId(), ioexception1);
}
// Spigot start
- io.papermc.paper.util.MCUtil.asyncExecutor.shutdown(); // Paper
- try { io.papermc.paper.util.MCUtil.asyncExecutor.awaitTermination(30, java.util.concurrent.TimeUnit.SECONDS); // Paper
- } catch (java.lang.InterruptedException ignored) {} // Paper
if (org.spigotmc.SpigotConfig.saveUserCacheOnStopOnly) {
MinecraftServer.LOGGER.info("Saving usercache.json");
this.getProfileCache().save(false); // Paper
@@ -982,6 +1106,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
LOGGER.info("Flushing Chunk IO");
io.papermc.paper.chunk.system.io.RegionFileIOThread.close(true); // Paper // Paper - rewrite chunk system
LOGGER.info("Closing Thread Pool");
+ // Gale start - base thread pools - remove Paper async executor, remove background executor
+ long startTime = Util.getMillis();
+ LOGGER.info("Waiting 30 seconds for asynchronous tasks to finish...");
+ serverThread.runTasksUntil(() -> Util.getMillis() - startTime >= 30 || !BaseTaskQueues.scheduledAsync.hasTasks(), null); // Paper
+ LOGGER.info("Shutting down IO executor...");
+ // Gale end - base thread pools - remove Paper async executor
+ // Gale end - base thread pools - remove background executor
Util.shutdownExecutors(); // Paper
LOGGER.info("Closing Server");
try {
@@ -1017,7 +1148,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
this.running = false;
if (waitForShutdown) {
try {
- this.serverThread.join();
+ serverThread.join(); // Gale - base thread pools
} catch (InterruptedException interruptedexception) {
MinecraftServer.LOGGER.error("Error while shutting down", interruptedexception);
}
@@ -1091,6 +1222,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
public static long lastTickOversleepTime;
// Gale end - YAPFA - last tick time
+ @OriginalServerThreadOnly // Gale - base thread pools
protected void runServer() {
try {
long serverStartTime = Util.getNanos(); // Paper
@@ -1098,7 +1230,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
throw new IllegalStateException("Failed to initialize server");
}
- this.nextTickTime = Util.getMillis();
+ this.setNextTickTime(Util.getMillis()); // Gale - base thread pools
this.status.setDescription(Component.literal(this.motd));
this.status.setVersion(new ServerStatus.Version(SharedConstants.getCurrentVersion().getName(), SharedConstants.getCurrentVersion().getProtocolVersion()));
this.status.setEnforcesSecureChat(this.enforceSecureProfile());
@@ -1135,7 +1267,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
if (this.server.getWarnOnOverload()) // CraftBukkit
MinecraftServer.LOGGER.warn("Can't keep up! Is the server overloaded? Running {}ms or {} ticks behind", i, j);
- this.nextTickTime += j * 50L;
+ this.setNextTickTime(this.nextTickTime + j * 50L); // Gale - base thread pools
this.lastOverloadWarning = this.nextTickTime;
}
@@ -1159,12 +1291,11 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
//MinecraftServer.currentTick = (int) (System.currentTimeMillis() / 50); // CraftBukkit // Paper - don't overwrite current tick time
lastTick = curTime;
- this.nextTickTime += 50L;
+ this.setNextTickTime(this.nextTickTime + 50L); // Gale - base thread pools
long tickProperStart = System.nanoTime(); // Gale - YAPFA - last tick time
this.tickServer(this::haveTime);
lastTickProperTime = (System.nanoTime() - tickProperStart) / 1000000L; // Gale - YAPFA - last tick time
- this.mayHaveDelayedTasks = true;
- this.delayedTasksMaxNextTickTime = Math.max(Util.getMillis() + 50L, this.nextTickTime);
+ this.setDelayedTasksMaxNextTickTime(Math.max(Util.getMillis() + 50L, this.nextTickTime)); // Gale - base thread pools
this.waitUntilNextTick();
this.isReady = true;
JvmProfiler.INSTANCE.onServerTick(this.averageTickTime);
@@ -1245,7 +1376,47 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
return crashreport;
}
- private boolean haveTime() {
+ // Gale start - base thread pools
+ private static final SignalReason noMoreDelayedTasksSignalReason = SignalReason.createNonRetrying();
+
+ /**
+ * The return value of this method is slightly heuristic in when it is computed: it may be invalidated by
+ * other threads during its execution or immediately upon returning.
+ *
+ * @return Whether there are potentially main-thread-only tasks scheduled in some queue.
+ */
+ private static boolean mayHaveDelayedTasks() {
+ // First check the flag to skip this check
+ if (nextTimeAssumeWeMayHaveDelayedTasks) {
+ nextTimeAssumeWeMayHaveDelayedTasks = false;
+ mayHaveDelayedTasksIsCurrentlyComputed = false;
+ return true;
+ }
+ // If we still have a valid computation result, use it
+ if (mayHaveDelayedTasksIsCurrentlyComputed) {
+ return lastComputedMayHaveDelayedTasks;
+ }
+ // Compute the result and save it
+ lastComputedMayHaveDelayedTasks = false;
+ if (ScheduledServerThreadTaskQueues.hasTasks(true)) {
+ lastComputedMayHaveDelayedTasks = true;
+ } else {
+ for (ServerLevel level : SERVER.getAllLevelsArray()) {
+ if (level.chunkSource.mainThreadProcessor.hasPendingTasks() || level.chunkTaskScheduler.mainThreadExecutor.hasScheduledUncompletedTasksVolatile()) {
+ lastComputedMayHaveDelayedTasks = true;
+ break;
+ }
+ }
+ }
+ mayHaveDelayedTasksIsCurrentlyComputed = true;
+ if (!lastComputedMayHaveDelayedTasks) {
+ serverThread.signal(noMoreDelayedTasksSignalReason);
+ }
+ return lastComputedMayHaveDelayedTasks;
+ }
+ // Gale end - base thread pools
+
+ public boolean haveTime() { // Gale - base thread pools - private -> public
// Paper start
if (this.forceTicks) {
return true;
@@ -1253,13 +1424,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
// Paper end
// CraftBukkit start
if (isOversleep) return canOversleep();// Paper - because of our changes, this logic is broken
- return this.forceTicks || this.runningTask() || Util.getMillis() < (this.mayHaveDelayedTasks ? this.delayedTasksMaxNextTickTime : this.nextTickTime);
+ return this.forceTicks || this.runningTask() || Util.getMillis() < (mayHaveDelayedTasks() ? this.delayedTasksMaxNextTickTime : this.nextTickTime); // Gale - base thread pools
}
// Paper start
- boolean isOversleep = false;
+ public volatile boolean isOversleep = false; // Gale - base thread pools - make fields volatile, package -> public
private boolean canOversleep() {
- return this.mayHaveDelayedTasks && Util.getMillis() < this.delayedTasksMaxNextTickTime;
+ return Util.getMillis() < this.delayedTasksMaxNextTickTime && mayHaveDelayedTasks(); // Gale - base thread pools
}
private boolean canSleepForTickNoOversleep() {
@@ -1268,7 +1439,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
// Paper end
private void executeModerately() {
- this.runAllTasks();
+ this.runAllMainThreadTasksForAllTicks(); // Gale - base thread pools
java.util.concurrent.locks.LockSupport.parkNanos("executing tasks", 1000L);
}
// CraftBukkit end
@@ -1276,61 +1447,20 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
protected void waitUntilNextTick() {
//this.executeAll(); // Paper - move this into the tick method for timings
long tickOversleepStart = System.nanoTime(); // Gale - YAPFA - last tick time
+ // Gale start - base thread pools
+ isWaitingUntilNextTick = true;
+ isInSpareTime = true;
+ // Gale end - base thread pools
this.managedBlock(() -> {
return !this.canSleepForTickNoOversleep(); // Paper - move oversleep into full server tick
+ // Gale start - base thread pools
});
+ isInSpareTime = false;
+ isWaitingUntilNextTick = false;
+ // Gale end - base thread pools
lastTickOversleepTime = (System.nanoTime() - tickOversleepStart) / 1000000L; // Gale - YAPFA - last tick time
}
- @Override
- public TickTask wrapRunnable(Runnable runnable) {
- // Paper start - anything that does try to post to main during watchdog crash, run on watchdog
- if (this.hasStopped && Thread.currentThread().equals(shutdownThread)) {
- runnable.run();
- runnable = () -> {};
- }
- // Paper end
- return new TickTask(this.tickCount, runnable);
- }
-
- protected boolean shouldRun(TickTask ticktask) {
- return ticktask.getTick() + 3 < this.tickCount || this.haveTime();
- }
-
- @Override
- public boolean pollTask() {
- boolean flag = this.pollTaskInternal();
-
- this.mayHaveDelayedTasks = flag;
- return flag;
- }
-
- private boolean pollTaskInternal() {
- if (super.pollTask()) {
- this.executeMidTickTasks(); // Paper - execute chunk tasks mid tick
- return true;
- } else {
- boolean ret = false; // Paper - force execution of all worlds, do not just bias the first
- if (this.haveTime()) {
- Iterator iterator = this.getAllLevels().iterator();
-
- while (iterator.hasNext()) {
- ServerLevel worldserver = (ServerLevel) iterator.next();
-
- if (worldserver.getChunkSource().pollTask()) {
- ret = true; // Paper - force execution of all worlds, do not just bias the first
- }
- }
- }
-
- return ret; // Paper - force execution of all worlds, do not just bias the first
- }
- }
-
- public void doRunTask(TickTask ticktask) { // CraftBukkit - decompile error
- super.doRunTask(ticktask);
- }
-
private void updateStatusIcon(ServerStatus metadata) {
Optional<File> optional = Optional.of(this.getFile("server-icon.png")).filter(File::isFile);
@@ -1378,14 +1508,19 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
// Paper start - move oversleep into full server tick
isOversleep = true;MinecraftTimings.serverOversleep.startTiming();
+ isInSpareTime = true; // Gale - base thread pools
this.managedBlock(() -> {
return !this.canOversleep();
+ // Gale start - base thread pools
});
+ isInSpareTime = false;
+ // Gale end - base thread pools
isOversleep = false;MinecraftTimings.serverOversleep.stopTiming();
// Paper end
new com.destroystokyo.paper.event.server.ServerTickStartEvent(this.tickCount+1).callEvent(); // Paper
++this.tickCount;
+ ScheduledServerThreadTaskQueues.shiftTasksForNextTick(); // Gale - base thread pools
this.tickChildren(shouldKeepTicking);
if (i - this.lastServerStatus >= 5000000000L) {
this.lastServerStatus = i;
@@ -1420,7 +1555,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
if (playerSaveInterval > 0) {
this.playerList.saveAll(playerSaveInterval);
}
- for (ServerLevel level : this.getAllLevels()) {
+ for (ServerLevel level : this.getAllLevelsArray()) { // Gale - base thread pools - optimize server levels
if (level.paperConfig().chunks.autoSaveInterval.value() > 0) {
level.saveIncrementally(fullSave);
}
@@ -1432,7 +1567,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
io.papermc.paper.util.CachedLists.reset(); // Paper
// Paper start - move executeAll() into full server tick timing
try (co.aikar.timings.Timing ignored = MinecraftTimings.processTasksTimer.startTiming()) {
- this.runAllTasks();
+ this.runAllTasksWithinTimeOrForCurrentTick();
}
// Paper end
// Paper start
@@ -1476,7 +1611,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
MinecraftTimings.timeUpdateTimer.startTiming(); // Spigot // Paper
// Send time updates to everyone, it will get the right time from the world the player is in.
// Paper start - optimize time updates
- for (final ServerLevel world : this.getAllLevels()) {
+ for (final ServerLevel world : this.getAllLevelsArray()) { // Gale - base thread pools - optimize server levels
final boolean doDaylight = world.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT);
final long dayTime = world.getDayTime();
long worldTime = world.getGameTime();
@@ -1496,9 +1631,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
MinecraftTimings.timeUpdateTimer.stopTiming(); // Spigot // Paper
this.isIteratingOverLevels = true; // Paper
- Iterator iterator = this.getAllLevels().iterator(); // Paper - move down
- while (iterator.hasNext()) {
- ServerLevel worldserver = (ServerLevel) iterator.next();
+ for (ServerLevel worldserver : this.getAllLevelsArray()) { // Paper - move down // Gale - base thread pools - optimize server levels
worldserver.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper
worldserver.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper
net.minecraft.world.level.block.entity.HopperBlockEntity.skipHopperEvents = worldserver.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper
@@ -1569,7 +1702,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
public boolean isShutdown() {
- return !this.serverThread.isAlive();
+ return !serverThread.isAlive(); // Gale - base thread pools
}
public File getFile(String path) {
@@ -1577,7 +1710,12 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
public final ServerLevel overworld() {
- return (ServerLevel) this.levels.get(Level.OVERWORLD);
+ // Gale start - base thread pools - optimize server levels
+ if (this.overworld == null) {
+ this.overworld = (ServerLevel) this.levels.get(Level.OVERWORLD);
+ }
+ return this.overworld;
+ // Gale end - base thread pools - optimize server levels
}
@Nullable
@@ -1591,6 +1729,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
Map<ResourceKey<Level>, ServerLevel> newLevels = Maps.newLinkedHashMap(oldLevels);
newLevels.put(level.dimension(), level);
this.levels = Collections.unmodifiableMap(newLevels);
+ // Gale start - base thread pools - optimize server levels
+ this.levelArray = newLevels.values().toArray(this.levelArray);
+ for (int i = 0; i < this.levelArray.length; i++) {
+ this.levelArray[i].serverLevelArrayIndex = i;
+ }
+ this.overworld = null;
+ // Gale end - base thread pools - optimize server levels
}
public void removeLevel(ServerLevel level) {
@@ -1598,6 +1743,14 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
Map<ResourceKey<Level>, ServerLevel> newLevels = Maps.newLinkedHashMap(oldLevels);
newLevels.remove(level.dimension());
this.levels = Collections.unmodifiableMap(newLevels);
+ // Gale start - base thread pools - optimize server levels
+ level.serverLevelArrayIndex = -1;
+ this.levelArray = newLevels.values().toArray(this.levelArray);
+ for (int i = 0; i < this.levelArray.length; i++) {
+ this.levelArray[i].serverLevelArrayIndex = i;
+ }
+ this.overworld = null;
+ // Gale end - base thread pools - optimize server levels
}
// CraftBukkit end
@@ -1605,8 +1758,14 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
return this.levels.keySet();
}
+ // Gale start - base thread pools - optimize server levels
+ public ServerLevel[] getAllLevelsArray() {
+ return this.levelArray;
+ }
+ // Gale end - base thread pools - optimize server levels
+
public Iterable<ServerLevel> getAllLevels() {
- return this.levels.values();
+ return this.levels == null ? Collections.emptyList() : this.levels.values(); // Gale - base thread pools
}
public String getServerVersion() {
@@ -1726,10 +1885,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
private void updateMobSpawningFlags() {
- Iterator iterator = this.getAllLevels().iterator();
-
- while (iterator.hasNext()) {
- ServerLevel worldserver = (ServerLevel) iterator.next();
+ for (ServerLevel worldserver : this.getAllLevelsArray()) { // Gale - base thread pools - optimize server levels
worldserver.setSpawnSettings(worldserver.serverLevelData.getDifficulty() != Difficulty.PEACEFUL && ((DedicatedServer) this).settings.getProperties().spawnMonsters, this.isSpawningAnimals()); // Paper - per level difficulty (from setDifficulty(ServerLevel, Difficulty, boolean))
}
@@ -1928,25 +2084,6 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
return 29999984;
}
- @Override
- public boolean scheduleExecutables() {
- return super.scheduleExecutables() && !this.isStopped();
- }
-
- @Override
- public void executeIfPossible(Runnable runnable) {
- if (this.isStopped()) {
- throw new RejectedExecutionException("Server already shutting down");
- } else {
- super.executeIfPossible(runnable);
- }
- }
-
- @Override
- public Thread getRunningThread() {
- return this.serverThread;
- }
-
public int getCompressionThreshold() {
return 256;
}
@@ -2013,7 +2150,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
net.minecraft.world.item.alchemy.PotionBrewing.reload(); // Paper
new io.papermc.paper.event.server.ServerResourcesReloadedEvent(cause).callEvent(); // Paper
// Paper start
- if (Thread.currentThread() != this.serverThread) {
+ if (Thread.currentThread() != serverThread) { // Gale - base thread pools
return;
}
// this.getPlayerList().saveAll(); // Paper - we don't need to save everything, just advancements
@@ -2246,7 +2383,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
BufferedWriter bufferedwriter = Files.newBufferedWriter(path);
try {
- bufferedwriter.write(String.format(Locale.ROOT, "pending_tasks: %d\n", this.getPendingTasksCount()));
+ bufferedwriter.write(String.format(Locale.ROOT, "pending_tasks: %d\n", ScheduledServerThreadTaskQueues.getTaskCount())); // Gale - base thread pools
bufferedwriter.write(String.format(Locale.ROOT, "average_tick_time: %f\n", this.getAverageTickTime()));
bufferedwriter.write(String.format(Locale.ROOT, "tick_times: %s\n", Arrays.toString(this.tickTimes)));
bufferedwriter.write(String.format(Locale.ROOT, "queue: %s\n", Util.backgroundExecutor()));
@@ -2432,7 +2569,6 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
// CraftBukkit start
- @Override
public boolean isSameThread() {
return io.papermc.paper.util.TickThread.isTickThread(); // Paper - rewrite chunk system
}
@@ -2570,7 +2706,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
// give all worlds a fair chance at by targetting them all.
// if we execute too many tasks, that's fine - we have logic to correctly handle overuse of allocated time.
boolean executed = false;
- for (ServerLevel world : this.getAllLevels()) {
+ for (ServerLevel world : this.getAllLevelsArray()) { // Gale - base thread pools - optimize server levels
long currTime = System.nanoTime();
if (currTime - world.lastMidTickExecuteFailure <= TASK_EXECUTION_FAILURE_BACKOFF) {
continue;
diff --git a/src/main/java/net/minecraft/server/commands/TimeCommand.java b/src/main/java/net/minecraft/server/commands/TimeCommand.java
index f0a7a8df3caa2ea765bb0a87cfede71d0995d276..59022fd5c784046ec65a473236a0788ea3e6f58a 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 pools - 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 pools - 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 pools - 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 pools - 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 eed9f125df46b616b7234a2d669971bc51bc231b..396258cb21a8e5488f0f456b65dc629ff895601e 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 pools
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 8ed18a158a13dceca7cd342125c09e52ac0d013c..d513245ec5fadfa511895feeef73d2a57bd21160 100644
--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
@@ -11,7 +11,6 @@ import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
-import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.BooleanSupplier;
@@ -22,6 +21,7 @@ import net.minecraft.Util;
import net.minecraft.core.BlockPos;
import net.minecraft.core.SectionPos;
import net.minecraft.network.protocol.Packet;
+import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.progress.ChunkProgressListener;
import net.minecraft.util.VisibleForDebug;
import net.minecraft.util.thread.BlockableEventLoop;
@@ -48,6 +48,7 @@ import net.minecraft.world.level.storage.DimensionDataStorage;
import net.minecraft.world.level.storage.LevelData;
import net.minecraft.world.level.storage.LevelStorageSource;
import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; // Paper
+import org.galemc.gale.executor.queue.BaseTaskQueues;
public class ServerChunkCache extends ChunkSource {
@@ -977,6 +978,14 @@ public class ServerChunkCache extends ChunkSource {
super.doRunTask(task);
}
+ // Gale start - base thread pools
+ @Override
+ public void tell(Runnable runnable) {
+ super.tell(runnable);
+ MinecraftServer.nextTimeAssumeWeMayHaveDelayedTasks = true;
+ BaseTaskQueues.allLevelsScheduledChunkCache.signalReason.signalAnother();
+ }
+
@Override
// CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task
public boolean pollTask() {
diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
index 37e0b6212fec71ec9662e6be3b1e8bea487eb4a6..251f098fa2203d06e5e5aa68a31a76532ee5d09c 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<ServerPlayer> players;
public final ServerChunkCache chunkSource;
+ // Gale start - base thread pools
+ @AnyThreadSafe(Access.READ)
+ public volatile int serverLevelArrayIndex;
+ // Gale end - base thread pools
private final MinecraftServer server;
public final PrimaryLevelData serverLevelData; // CraftBukkit - type
final EntityTickList entityTickList;
@@ -2558,7 +2566,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
// Spigot start
if ( entity instanceof Player )
{
- com.google.common.collect.Streams.stream( ServerLevel.this.getServer().getAllLevels() ).map( ServerLevel::getDataStorage ).forEach( (worldData) ->
+ Arrays.stream( ServerLevel.this.getServer().getAllLevelsArray() ).map( ServerLevel::getDataStorage ).forEach( (worldData) -> // Gale - base thread pools - optimize server levels
{
for (Object o : worldData.cache.values() )
{
diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
index 315c7737f75c426a7e5c091fb340187df12235de..99e684ae2f5ac07660e4f724b15cf1bba185d438 100644
--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
@@ -186,6 +186,8 @@ import net.minecraft.world.phys.shapes.Shapes;
import net.minecraft.world.phys.shapes.VoxelShape;
import org.apache.commons.lang3.StringUtils;
import org.galemc.gale.configuration.GaleGlobalConfiguration;
+import org.galemc.gale.executor.queue.BaseTaskQueues;
+import org.galemc.gale.executor.queue.ScheduledServerThreadTaskQueues;
import org.slf4j.Logger;
// CraftBukkit start
@@ -544,7 +546,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
Objects.requireNonNull(this.connection);
// CraftBukkit - Don't wait
- minecraftserver.scheduleOnMain(networkmanager::handleDisconnection); // Paper
+ ScheduledServerThreadTaskQueues.add(networkmanager::handleDisconnection, ScheduledServerThreadTaskQueues.HANDLE_DISCONNECT_TASK_MAX_DELAY); // Paper // Gale - base thread pools
}
private <T, R> CompletableFuture<R> filterTextPacket(T text, BiFunction<TextFilter, T, CompletableFuture<R>> filterer) {
@@ -875,21 +877,20 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
}
// Paper start
- private static final java.util.concurrent.ExecutorService TAB_COMPLETE_EXECUTOR = java.util.concurrent.Executors.newFixedThreadPool(4,
- new com.google.common.util.concurrent.ThreadFactoryBuilder().setDaemon(true).setNameFormat("Async Tab Complete Thread - #%d").setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(net.minecraft.server.MinecraftServer.LOGGER)).build());
+ private static final java.util.concurrent.ExecutorService TAB_COMPLETE_EXECUTOR = BaseTaskQueues.scheduledAsync.yieldingExecutor; // Gale - base thread pools - remove tab complete executor
// Paper end
@Override
public void handleCustomCommandSuggestions(ServerboundCommandSuggestionPacket packet) {
// PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel()); // Paper - run this async
// CraftBukkit start
if (this.chatSpamTickCount.addAndGet(io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.tabSpamIncrement) > io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.tabSpamLimit && !this.server.getPlayerList().isOp(this.player.getGameProfile())) { // Paper start - split and make configurable
- server.scheduleOnMain(() -> this.disconnect(Component.translatable("disconnect.spam", ArrayConstants.emptyObjectArray), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM)); // Paper - kick event cause // Gale - JettPack - reduce array allocations
+ ScheduledServerThreadTaskQueues.add(() -> this.disconnect(Component.translatable("disconnect.spam", ArrayConstants.emptyObjectArray), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM), ScheduledServerThreadTaskQueues.KICK_FOR_COMMAND_PACKET_SPAM_TASK_MAX_DELAY); // Paper - kick event cause // Gale - JettPack - reduce array allocations // Gale - base thread pools
return;
}
// Paper start
String str = packet.getCommand(); int index = -1;
if (str.length() > 64 && ((index = str.indexOf(' ')) == -1 || index >= 64)) {
- server.scheduleOnMain(() -> this.disconnect(Component.translatable("disconnect.spam", ArrayConstants.emptyObjectArray), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM)); // Paper - kick event cause // Gale - JettPack - reduce array allocations
+ ScheduledServerThreadTaskQueues.add(() -> this.disconnect(Component.translatable("disconnect.spam", ArrayConstants.emptyObjectArray), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM), ScheduledServerThreadTaskQueues.KICK_FOR_COMMAND_PACKET_SPAM_TASK_MAX_DELAY); // Paper - kick event cause // Gale - JettPack - reduce array allocations // Gale - base thread pools
return;
}
// Paper end
@@ -914,7 +915,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
if (!event.isHandled()) {
if (!event.isCancelled()) {
- this.server.scheduleOnMain(() -> { // This needs to be on main
+ ScheduledServerThreadTaskQueues.add(() -> { // This needs to be on main // Gale - base thread pools
ParseResults<CommandSourceStack> parseresults = this.server.getCommands().getDispatcher().parse(stringreader, this.player.createCommandSourceStack());
this.server.getCommands().getDispatcher().getCompletionSuggestions(parseresults).thenAccept((suggestions) -> {
@@ -925,7 +926,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
this.connection.send(new ClientboundCommandSuggestionsPacket(packet.getId(), suggestEvent.getSuggestions()));
// Paper end - Brigadier API
});
- });
+ }, ScheduledServerThreadTaskQueues.SEND_COMMAND_COMPLETION_SUGGESTIONS_TASK_MAX_DELAY); // Gale - base thread pools
}
} else if (!completions.isEmpty()) {
final com.mojang.brigadier.suggestion.SuggestionsBuilder builder0 = new com.mojang.brigadier.suggestion.SuggestionsBuilder(command, stringreader.getTotalLength());
@@ -1234,7 +1235,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
int byteLength = testString.getBytes(java.nio.charset.StandardCharsets.UTF_8).length;
if (byteLength > 256 * 4) {
ServerGamePacketListenerImpl.LOGGER.warn(this.player.getScoreboardName() + " tried to send a book with with a page too large!");
- server.scheduleOnMain(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause
+ ScheduledServerThreadTaskQueues.add(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION), ScheduledServerThreadTaskQueues.KICK_FOR_BOOK_TOO_LARGE_PACKET_TASK_MAX_DELAY); // Paper - kick event cause // Gale - base thread pools
return;
}
byteTotal += byteLength;
@@ -1257,14 +1258,14 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
if (byteTotal > byteAllowed) {
ServerGamePacketListenerImpl.LOGGER.warn(this.player.getScoreboardName() + " tried to send too large of a book. Book Size: " + byteTotal + " - Allowed: "+ byteAllowed + " - Pages: " + pageList.size());
- server.scheduleOnMain(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause
+ ScheduledServerThreadTaskQueues.add(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION), ScheduledServerThreadTaskQueues.KICK_FOR_BOOK_TOO_LARGE_PACKET_TASK_MAX_DELAY); // Paper - kick event cause // Gale - base thread pools
return;
}
}
// Paper end
// CraftBukkit start
if (this.lastBookTick + 20 > MinecraftServer.currentTick) {
- server.scheduleOnMain(() -> this.disconnect("Book edited too quickly!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause // Paper - Also ensure this is called on main
+ ScheduledServerThreadTaskQueues.add(() -> this.disconnect("Book edited too quickly!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION), ScheduledServerThreadTaskQueues.KICK_FOR_EDITING_BOOK_TOO_QUICKLY_TASK_MAX_DELAY); // Paper - kick event cause // Paper - Also ensure this is called on main // Gale - base thread pools
return;
}
this.lastBookTick = MinecraftServer.currentTick;
@@ -2068,10 +2069,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
public void handleTeleportToEntityPacket(ServerboundTeleportToEntityPacket packet) {
PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
if (this.player.isSpectator()) {
- Iterator iterator = this.server.getAllLevels().iterator();
-
- while (iterator.hasNext()) {
- ServerLevel worldserver = (ServerLevel) iterator.next();
+ for (ServerLevel worldserver : this.server.getAllLevelsArray()) { // Gale - base thread pools - optimize server levels
Entity entity = packet.getEntity(worldserver);
if (entity != null) {
@@ -2220,9 +2218,9 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
}
// CraftBukkit end
if (ServerGamePacketListenerImpl.isChatMessageIllegal(packet.message())) {
- this.server.scheduleOnMain(() -> { // Paper - push to main for event firing
+ ScheduledServerThreadTaskQueues.add(() -> { // Paper - push to main for event firing // Gale - base thread pools
this.disconnect(Component.translatable("multiplayer.disconnect.illegal_characters"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_CHARACTERS); // Paper - add cause
- }); // Paper - push to main for event firing
+ }, ScheduledServerThreadTaskQueues.KICK_FOR_ILLEGAL_CHARACTERS_IN_CHAT_PACKET_TASK_MAX_DELAY); // Paper - push to main for event firing // Gale - base thread pools
} else {
Optional<LastSeenMessages> optional = this.tryHandleChat(packet.message(), packet.timeStamp(), packet.lastSeenMessages());
@@ -2256,9 +2254,9 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
@Override
public void handleChatCommand(ServerboundChatCommandPacket packet) {
if (ServerGamePacketListenerImpl.isChatMessageIllegal(packet.command())) {
- this.server.scheduleOnMain(() -> { // Paper - push to main for event firing
+ ScheduledServerThreadTaskQueues.add(() -> { // Paper - push to main for event firing // Gale - base thread pools
this.disconnect(Component.translatable("multiplayer.disconnect.illegal_characters"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_CHARACTERS); // Paper
- }); // Paper - push to main for event firing
+ }, ScheduledServerThreadTaskQueues.KICK_FOR_ILLEGAL_CHARACTERS_IN_CHAT_PACKET_TASK_MAX_DELAY); // Paper - push to main for event firing // Gale - base thread pools
} else {
Optional<LastSeenMessages> optional = this.tryHandleChat(packet.command(), packet.timeStamp(), packet.lastSeenMessages());
@@ -2338,9 +2336,9 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
private Optional<LastSeenMessages> tryHandleChat(String message, Instant timestamp, LastSeenMessages.Update acknowledgment) {
if (!this.updateChatOrder(timestamp)) {
ServerGamePacketListenerImpl.LOGGER.warn("{} sent out-of-order chat: '{}': {} > {}", this.player.getName().getString(), message, this.lastChatTimeStamp.get().getEpochSecond(), timestamp.getEpochSecond()); // Paper
- this.server.scheduleOnMain(() -> { // Paper - push to main
- this.disconnect(Component.translatable("multiplayer.disconnect.out_of_order_chat"), org.bukkit.event.player.PlayerKickEvent.Cause.OUT_OF_ORDER_CHAT); // Paper - kick event ca
- }); // Paper - push to main
+ ScheduledServerThreadTaskQueues.add(() -> { // Paper - push to main // Gale - base thread pools
+ this.disconnect(Component.translatable("multiplayer.disconnect.out_of_order_chat"), org.bukkit.event.player.PlayerKickEvent.Cause.OUT_OF_ORDER_CHAT); // Paper - kick event cause
+ }, ScheduledServerThreadTaskQueues.KICK_FOR_OUT_OF_ORDER_CHAT_PACKET_TASK_MAX_DELAY); // Paper - push to main // Gale - base thread pools
return Optional.empty();
} else if (this.player.isRemoved() || this.player.getChatVisibility() == ChatVisiblity.HIDDEN) { // CraftBukkit - dead men tell no tales
this.send(new ClientboundSystemChatPacket(Component.translatable("chat.disabled.options").withStyle(ChatFormatting.RED), false));
@@ -3270,7 +3268,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
// Paper start
if (!org.bukkit.Bukkit.isPrimaryThread()) {
if (recipeSpamPackets.addAndGet(io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.recipeSpamIncrement) > io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.recipeSpamLimit) {
- server.scheduleOnMain(() -> this.disconnect(net.minecraft.network.chat.Component.translatable("disconnect.spam", ArrayConstants.emptyObjectArray), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM)); // Paper - kick event cause // Gale - JettPack - reduce array allocations
+ ScheduledServerThreadTaskQueues.add(() -> this.disconnect(net.minecraft.network.chat.Component.translatable("disconnect.spam", ArrayConstants.emptyObjectArray), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM), ScheduledServerThreadTaskQueues.KICK_FOR_RECIPE_PACKET_SPAM_TASK_MAX_DELAY); // Paper - kick event cause // Gale - JettPack - reduce array allocations // Gale - base thread pools
return;
}
}
diff --git a/src/main/java/net/minecraft/server/network/TextFilterClient.java b/src/main/java/net/minecraft/server/network/TextFilterClient.java
index 4b3d2280326c7eeda4952c36edff141cbff90e16..fa3a58f09178604e301b107f1a029e59a7164e13 100644
--- a/src/main/java/net/minecraft/server/network/TextFilterClient.java
+++ b/src/main/java/net/minecraft/server/network/TextFilterClient.java
@@ -23,7 +23,6 @@ import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nullable;
@@ -32,6 +31,7 @@ import net.minecraft.Util;
import net.minecraft.network.chat.FilterMask;
import net.minecraft.util.GsonHelper;
import net.minecraft.util.thread.ProcessorMailbox;
+import org.galemc.gale.executor.queue.BaseTaskQueues;
import org.slf4j.Logger;
public class TextFilterClient implements AutoCloseable {
@@ -62,7 +62,7 @@ public class TextFilterClient implements AutoCloseable {
this.joinEncoder = joinEncoder;
this.leaveEndpoint = leaveEndpoint;
this.leaveEncoder = leaveEncoder;
- this.workerPool = Executors.newFixedThreadPool(parallelism, THREAD_FACTORY);
+ this.workerPool = BaseTaskQueues.scheduledAsync.yieldingExecutor; // Gale - base thread pools - remove text filter executor
}
private static URL getEndpoint(URI root, @Nullable JsonObject endpoints, String key, String fallback) throws MalformedURLException {
diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
index b34d64de19f387b4a95cc24b57aa98f81c4f2a9c..636a5afca868b05f7dbb4da8a7e04b968ada5048 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;
@@ -289,6 +288,58 @@ public abstract class PlayerList {
player.getRecipeBook().sendInitialRecipeBook(player);
this.updateEntireScoreboard(worldserver1.getScoreboard(), player);
this.server.invalidateStatus();
+/* // Gale - base thread pools - this patch was removed from Paper but might be useful later
+ // Paper start - async load spawn in chunk
+ ServerLevel finalWorldserver = worldserver1;
+ finalWorldserver.pendingLogin.add(player);
+ int chunkX = loc.getBlockX() >> 4;
+ int chunkZ = loc.getBlockZ() >> 4;
+ final net.minecraft.world.level.ChunkPos pos = new net.minecraft.world.level.ChunkPos(chunkX, chunkZ);
+ net.minecraft.server.level.ChunkMap playerChunkMap = worldserver1.getChunkSource().chunkMap;
+ net.minecraft.server.level.DistanceManager distanceManager = playerChunkMap.distanceManager;
+ io.papermc.paper.chunk.system.ChunkSystem.scheduleTickingState(
+ worldserver1, chunkX, chunkZ, net.minecraft.server.level.ChunkHolder.FullChunkStatus.ENTITY_TICKING, true,
+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHEST,
+ (chunk) -> {
+ ScheduledServerThreadTaskQueues.add(() -> { // Gale - base thread pools
+ try {
+ if (!playerconnection.connection.isConnected()) {
+ return;
+ }
+ PlayerList.this.postChunkLoadJoin(
+ player, finalWorldserver, connection, playerconnection,
+ nbttagcompound, s1, lastKnownName
+ );
+ distanceManager.addTicket(net.minecraft.server.level.TicketType.LOGIN, pos, 31, pos.toLong());
+ } finally {
+ finalWorldserver.pendingLogin.remove(player);
+ }
+ }, ScheduledServerThreadTaskQueues.POST_CHUNK_LOAD_JOIN_TASK_MAX_DELAY); // Gale - base thread pools
+ }
+ );
+ }
+
+ public ServerPlayer getActivePlayer(UUID uuid) {
+ ServerPlayer player = this.playersByUUID.get(uuid);
+ return player != null ? player : pendingPlayers.get(uuid);
+ }
+
+ void disconnectPendingPlayer(ServerPlayer entityplayer) {
+ Component msg = Component.translatable("multiplayer.disconnect.duplicate_login");
+ entityplayer.networkManager.send(new net.minecraft.network.protocol.game.ClientboundDisconnectPacket(msg), net.minecraft.network.PacketSendListener.thenRun(() -> {
+ entityplayer.networkManager.disconnect(msg);
+ entityplayer.networkManager = null;
+ }));
+ }
+
+ private void postChunkLoadJoin(ServerPlayer player, ServerLevel worldserver1, Connection networkmanager, ServerGamePacketListenerImpl playerconnection, CompoundTag nbttagcompound, String s1, String s) {
+ pendingPlayers.remove(player.getUUID(), player);
+ if (!networkmanager.isConnected()) {
+ return;
+ }
+ player.didPlayerJoinEvent = true;
+ // Paper end
+*/ // Gale - base thread pools - this patch was removed from Paper but might be useful later
MutableComponent ichatmutablecomponent;
if (player.getGameProfile().getName().equalsIgnoreCase(s)) {
@@ -1494,10 +1545,8 @@ public abstract class PlayerList {
public void setViewDistance(int viewDistance) {
this.viewDistance = viewDistance;
//this.broadcastAll(new ClientboundSetChunkCacheRadiusPacket(viewDistance)); // Paper - move into setViewDistance
- Iterator iterator = this.server.getAllLevels().iterator();
- while (iterator.hasNext()) {
- ServerLevel worldserver = (ServerLevel) iterator.next();
+ for (ServerLevel worldserver : this.server.getAllLevelsArray()) { // Gale - base thread pools - optimize server levels
if (worldserver != null) {
worldserver.getChunkSource().setViewDistance(viewDistance);
@@ -1509,10 +1558,8 @@ public abstract class PlayerList {
public void setSimulationDistance(int simulationDistance) {
this.simulationDistance = simulationDistance;
//this.broadcastAll(new ClientboundSetSimulationDistancePacket(simulationDistance)); // Paper - handled by playerchunkloader
- Iterator iterator = this.server.getAllLevels().iterator();
- while (iterator.hasNext()) {
- ServerLevel worldserver = (ServerLevel) iterator.next();
+ for (ServerLevel worldserver : this.server.getAllLevelsArray()) { // Gale - base thread pools - optimize server levels
if (worldserver != null) {
worldserver.getChunkSource().setSimulationDistance(simulationDistance);
diff --git a/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java b/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java
index 83701fbfaa56a232593ee8f11a3afb8941238bfa..321be4cfea7228f5f5131eb521daa67590f00078 100644
--- a/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java
+++ b/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java
@@ -6,17 +6,18 @@ import com.mojang.logging.LogUtils;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.Executor;
import java.util.concurrent.locks.LockSupport;
import java.util.function.BooleanSupplier;
import java.util.function.Supplier;
+
import net.minecraft.util.profiling.metrics.MetricCategory;
import net.minecraft.util.profiling.metrics.MetricSampler;
import net.minecraft.util.profiling.metrics.MetricsRegistry;
import net.minecraft.util.profiling.metrics.ProfilerMeasured;
+import org.galemc.gale.executor.AbstractBlockableEventLoop;
import org.slf4j.Logger;
-public abstract class BlockableEventLoop<R extends Runnable> implements ProfilerMeasured, ProcessorHandle<R>, Executor {
+public abstract class BlockableEventLoop<R extends Runnable> implements ProfilerMeasured, ProcessorHandle<R>, AbstractBlockableEventLoop { // Gale - base thread pools
private final String name;
private static final Logger LOGGER = LogUtils.getLogger();
private final Queue<R> pendingRunnables = Queues.newConcurrentLinkedQueue();
@@ -31,6 +32,7 @@ public abstract class BlockableEventLoop<R extends Runnable> implements Profiler
protected abstract boolean shouldRun(R task);
+ @Override // Gale - base thread pools
public boolean isSameThread() {
return Thread.currentThread() == this.getRunningThread();
}
@@ -45,6 +47,12 @@ public abstract class BlockableEventLoop<R extends Runnable> implements Profiler
return this.pendingRunnables.size();
}
+ // Gale start - base thread pools
+ public boolean hasPendingTasks() {
+ return !this.pendingRunnables.isEmpty();
+ }
+ // Gale end - base thread pools
+
@Override
public String name() {
return this.name;
@@ -102,6 +110,7 @@ public abstract class BlockableEventLoop<R extends Runnable> implements Profiler
}
+ @Override // Gale - base thread pools
public void executeIfPossible(Runnable runnable) {
this.execute(runnable);
}
diff --git a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java
index 9948cc4c65d5681c171b38cdf7cf3e63a01e4364..c37793871951b0044168610bc05ee0529f3c4611 100644
--- a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java
+++ b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java
@@ -98,7 +98,7 @@ public abstract class Projectile extends Entity {
this.cachedOwner = ((ServerLevel) this.level).getEntity(this.ownerUUID);
// Paper start - check all worlds
if (this.cachedOwner == null) {
- for (final ServerLevel level : this.level.getServer().getAllLevels()) {
+ for (final ServerLevel level : this.level.getServer().getAllLevelsArray()) { // Gale - base thread pools - optimize server levels
if (level == this.level) continue;
final Entity entity = level.getEntity(this.ownerUUID);
if (entity != null) {
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
index e23fdd5ba09b50b7eef0ca4f36c5480779fba624..79f3a6174873834de61d7dc9fdbf6eb5a0fd6cd9 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
@@ -986,7 +986,7 @@ public final class CraftServer implements Server {
org.spigotmc.SpigotConfig.init((File) console.options.valueOf("spigot-settings")); // Spigot
this.console.paperConfigurations.reloadConfigs(this.console);
this.console.galeConfigurations.reloadConfigs(this.console); // Gale - Gale configuration
- for (ServerLevel world : this.console.getAllLevels()) {
+ for (ServerLevel world : this.console.getAllLevelsArray()) { // Gale - base thread pools - optimize server levels
// world.serverLevelData.setDifficulty(config.difficulty); // Paper - per level difficulty
world.setSpawnSettings(world.serverLevelData.getDifficulty() != Difficulty.PEACEFUL && config.spawnMonsters, config.spawnAnimals); // Paper - per level difficulty (from MinecraftServer#setDifficulty(ServerLevel, Difficulty, boolean))
@@ -1170,7 +1170,7 @@ public final class CraftServer implements Server {
@Override
public World createWorld(WorldCreator creator) {
- Preconditions.checkState(this.console.getAllLevels().iterator().hasNext(), "Cannot create additional worlds on STARTUP");
+ Preconditions.checkState(this.console.getAllLevelsArray().length > 0, "Cannot create additional worlds on STARTUP"); // Gale - base thread pools - optimize server levels
//Preconditions.checkState(!this.console.isIteratingOverLevels, "Cannot create a world while worlds are being ticked"); // Paper - Cat - Temp disable. We'll see how this goes.
Validate.notNull(creator, "Creator may not be null");
@@ -2526,7 +2526,7 @@ public final class CraftServer implements Server {
public Entity getEntity(UUID uuid) {
Validate.notNull(uuid, "UUID cannot be null");
- for (ServerLevel world : this.getServer().getAllLevels()) {
+ for (ServerLevel world : this.getServer().getAllLevelsArray()) { // Gale - base thread pools - optimize server levels
net.minecraft.world.entity.Entity entity = world.getEntity(uuid);
if (entity != null) {
return entity.getBukkitEntity();
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
index f8d321e925bf2708e51590542325c1bdc67d5964..a190bb9ce7b3701963f315452359f6f9c3aae329 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
@@ -5,7 +5,6 @@ import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.mojang.datafixers.util.Pair;
-import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import java.io.File;
@@ -20,7 +19,6 @@ import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
-import java.util.concurrent.ExecutionException;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import net.minecraft.core.BlockPos;
@@ -114,7 +112,6 @@ import org.bukkit.entity.TippedArrow;
import org.bukkit.entity.Trident;
import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason;
import org.bukkit.event.weather.LightningStrikeEvent;
-import org.bukkit.event.world.SpawnChangeEvent;
import org.bukkit.event.world.TimeSkipEvent;
import org.bukkit.generator.BiomeProvider;
import org.bukkit.generator.BlockPopulator;
@@ -134,6 +131,7 @@ import org.bukkit.util.Consumer;
import org.bukkit.util.RayTraceResult;
import org.bukkit.util.StructureSearchResult;
import org.bukkit.util.Vector;
+import org.galemc.gale.executor.queue.ScheduledServerThreadTaskQueues;
public class CraftWorld extends CraftRegionAccessor implements World {
public static final int CUSTOM_DIMENSION_OFFSET = 10;
@@ -2356,11 +2354,11 @@ public class CraftWorld extends CraftRegionAccessor implements World {
java.util.concurrent.CompletableFuture<Chunk> ret = new java.util.concurrent.CompletableFuture<>();
io.papermc.paper.chunk.system.ChunkSystem.scheduleChunkLoad(this.getHandle(), x, z, gen, ChunkStatus.FULL, true, priority, (c) -> {
- net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> {
+ ScheduledServerThreadTaskQueues.add(() -> { // Gale - base thread pools
net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk)c;
if (chunk != null) addTicket(x, z); // Paper
ret.complete(chunk == null ? null : chunk.getBukkitChunk());
- });
+ }, ScheduledServerThreadTaskQueues.COMPLETE_CHUNK_FUTURE_TASK_MAX_DELAY); // Gale - base thread pools
});
return ret;
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
index 78f53ee557276de85f0431ebcb146445b1f4fb92..c8b0a191832523e6c2e0fe4fd6cb1b8fa5104f86 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
@@ -190,6 +190,7 @@ import org.bukkit.plugin.Plugin;
import org.bukkit.util.BoundingBox;
import org.bukkit.util.NumberConversions;
import org.bukkit.util.Vector;
+import org.galemc.gale.executor.queue.ScheduledServerThreadTaskQueues;
public abstract class CraftEntity implements org.bukkit.entity.Entity {
private static PermissibleBase perm;
@@ -1280,7 +1281,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
for (net.minecraft.world.level.chunk.ChunkAccess chunk : list) {
chunkProviderServer.addTicketAtLevel(net.minecraft.server.level.TicketType.POST_TELEPORT, chunk.getPos(), 33, CraftEntity.this.getEntityId());
}
- net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> {
+ ScheduledServerThreadTaskQueues.add(() -> { // Gale - base thread pools
try {
ret.complete(CraftEntity.this.teleport(locationClone, cause) ? Boolean.TRUE : Boolean.FALSE);
} catch (Throwable throwable) {
@@ -1290,7 +1291,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
net.minecraft.server.MinecraftServer.LOGGER.error("Failed to teleport entity " + CraftEntity.this, throwable);
ret.completeExceptionally(throwable);
}
- });
+ }, ScheduledServerThreadTaskQueues.TELEPORT_ASYNC_TASK_MAX_DELAY); // Gale - base thread pools
});
return ret;
diff --git a/src/main/java/org/galemc/gale/concurrent/LockAndCondition.java b/src/main/java/org/galemc/gale/concurrent/LockAndCondition.java
index 73fd8ca0bd1168862a03d9bdcae93d62895e8c1f..6985e86495fc7046cbe1623d90f72adc95d909e2 100644
--- a/src/main/java/org/galemc/gale/concurrent/LockAndCondition.java
+++ b/src/main/java/org/galemc/gale/concurrent/LockAndCondition.java
@@ -9,7 +9,7 @@ import java.util.concurrent.locks.Lock;
* A utility class that stores a {@link Condition} with its {@link Lock}, that can be passed around and used instead
* of using an {@link Object} monitor, which does not support speculative locking.
*
- * @author Martijn Muijsers
+ * @author Martijn Muijsers under AGPL-3.0
*/
public class LockAndCondition {
diff --git a/src/main/java/org/galemc/gale/concurrent/Mutex.java b/src/main/java/org/galemc/gale/concurrent/Mutex.java
index 65ec8cf910575dfa4c5024ec69b3be1ef2634722..174c248aa706f6b5f3e248cb7604b44a4d508967 100644
--- a/src/main/java/org/galemc/gale/concurrent/Mutex.java
+++ b/src/main/java/org/galemc/gale/concurrent/Mutex.java
@@ -17,7 +17,7 @@ import java.util.concurrent.locks.Lock;
* respectively {@link #acquireUninterruptibly}, {@link #acquire}, {@link #tryAcquire} and
* {@link #release}. The {@link Lock#newCondition} method does not have a default implementation.
*
- * @author Martijn Muijsers
+ * @author Martijn Muijsers under AGPL-3.0
*/
@AnyThreadSafe
public interface Mutex extends Lock {
diff --git a/src/main/java/org/galemc/gale/concurrent/SemaphoreMutex.java b/src/main/java/org/galemc/gale/concurrent/SemaphoreMutex.java
index 2e31501d26b141729c80975e97a23b09653ba3bf..5a454236073dd75ed36d058c0f033c4aada403e3 100644
--- a/src/main/java/org/galemc/gale/concurrent/SemaphoreMutex.java
+++ b/src/main/java/org/galemc/gale/concurrent/SemaphoreMutex.java
@@ -15,7 +15,7 @@ import java.util.concurrent.locks.Lock;
* and throws {@link UnsupportedOperationException} for all {@link Lock} methods that do not have a default
* implementation in {@link Mutex}.
*
- * @author Martijn Muijsers
+ * @author Martijn Muijsers under AGPL-3.0
*/
@AnyThreadSafe
@YieldFree
diff --git a/src/main/java/org/galemc/gale/configuration/GaleConfigurations.java b/src/main/java/org/galemc/gale/configuration/GaleConfigurations.java
index 69acbab61a79c24312359a63086f9353d740113f..49ace73d901b6f55545bb21a93d026a04c5757ad 100644
--- a/src/main/java/org/galemc/gale/configuration/GaleConfigurations.java
+++ b/src/main/java/org/galemc/gale/configuration/GaleConfigurations.java
@@ -267,7 +267,7 @@ public class GaleConfigurations extends Configurations<GaleGlobalConfiguration,
try {
this.initializeGlobalConfiguration(reloader(this.globalConfigClass, GaleGlobalConfiguration.get()));
this.initializeWorldDefaultsConfiguration();
- for (ServerLevel level : server.getAllLevels()) {
+ for (ServerLevel level : server.getAllLevelsArray()) { // Gale - base thread pools - optimize server levels
this.createWorldConfig(PaperConfigurations.createWorldContextMap(level), reloader(this.worldConfigClass, level.galeConfig()));
}
} catch (Exception ex) {
diff --git a/src/main/java/org/galemc/gale/configuration/GaleGlobalConfiguration.java b/src/main/java/org/galemc/gale/configuration/GaleGlobalConfiguration.java
index 5b4f9956094f20daf9864b679b283448f357fecc..a00c6ba256ddbcb6330e2b00ef361200df08036a 100644
--- a/src/main/java/org/galemc/gale/configuration/GaleGlobalConfiguration.java
+++ b/src/main/java/org/galemc/gale/configuration/GaleGlobalConfiguration.java
@@ -2,11 +2,14 @@
package org.galemc.gale.configuration;
+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
import io.papermc.paper.configuration.Configuration;
import io.papermc.paper.configuration.ConfigurationPart;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
+import org.galemc.gale.executor.queue.ScheduledServerThreadTaskQueues;
import org.spongepowered.configurate.objectmapping.meta.Setting;
+import java.util.Arrays;
import java.util.Locale;
import java.util.function.Consumer;
@@ -59,6 +62,223 @@ public class GaleGlobalConfiguration extends ConfigurationPart {
}
// Gale end - Pufferfish - SIMD support
+ // Gale start - base thread pools
+ public MainThreadTaskMaxDelay mainThreadTaskMaxDelay;
+ public class MainThreadTaskMaxDelay extends ConfigurationPart.Post {
+
+ /**
+ * The default maximum delay for tasks.
+ * Given in ticks.
+ * Any value < 0 uses the vanilla maximum delay for tasks, which is currently 2.
+ * <ul>
+ * <li><i>Default</i>: -1</li>
+ * <li><i>Vanilla</i>: -1</li>
+ * </ul>
+ */
+ @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}.
+ * <ul>
+ * <li><i>Default</i>: 0</li>
+ * <li><i>Vanilla</i>: -1</li>
+ * </ul>
+ */
+ 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}.
+ * <ul>
+ * <li><i>Default</i>: 19</li>
+ * <li><i>Vanilla</i>: -1</li>
+ * </ul>
+ */
+ 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}.
+ * <ul>
+ * <li><i>Default</i>: 19</li>
+ * <li><i>Vanilla</i>: -1</li>
+ * </ul>
+ */
+ 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}.
+ * <ul>
+ * <li><i>Default</i>: -1</li>
+ * <li><i>Vanilla</i>: -1</li>
+ * </ul>
+ */
+ public int teleportAsync = -1;
+
+ /**
+ * The default maximum delay for command completion suggestions to be sent to the player.
+ * Any value < 0 uses {@link #defaultValue}.
+ * <ul>
+ * <li><i>Default</i>: 9</li>
+ * <li><i>Vanilla</i>: -1</li>
+ * </ul>
+ */
+ 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}.
+ * <ul>
+ * <li><i>Default</i>: 0</li>
+ * <li><i>Vanilla</i>: -1</li>
+ * </ul>
+ */
+ 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}.
+ * <ul>
+ * <li><i>Default</i>: 0</li>
+ * <li><i>Vanilla</i>: -1</li>
+ * </ul>
+ */
+ 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}.
+ * <ul>
+ * <li><i>Default</i>: -1</li>
+ * <li><i>Vanilla</i>: -1</li>
+ * </ul>
+ */
+ 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}.
+ * <ul>
+ * <li><i>Default</i>: -1</li>
+ * <li><i>Vanilla</i>: -1</li>
+ * </ul>
+ */
+ 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}.
+ * <ul>
+ * <li><i>Default</i>: -1</li>
+ * <li><i>Vanilla</i>: -1</li>
+ * </ul>
+ */
+ 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}.
+ * <ul>
+ * <li><i>Default</i>: -1</li>
+ * <li><i>Vanilla</i>: -1</li>
+ * </ul>
+ */
+ public int kickForOutOfOrderChatPacket = -1;
+
+ /**
+ * The default maximum delay for handling player disconnects.
+ * Any value < 0 uses {@link #defaultValue}.
+ * <ul>
+ * <li><i>Default</i>: -1</li>
+ * <li><i>Vanilla</i>: -1</li>
+ * </ul>
+ */
+ public int handleDisconnect = -1;
+
+ @Override
+ public void postProcess() {
+ while (!ScheduledServerThreadTaskQueues.writeLock.tryLock());
+ try {
+ // Update the values in MinecraftServerBlockableEventLoop for quick access
+ ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY = this.defaultValue >= 0 ? this.defaultValue : 2;
+ ScheduledServerThreadTaskQueues.COMPLETE_CHUNK_FUTURE_TASK_MAX_DELAY = this.completeChunkFuture >= 0 ? this.completeChunkFuture : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY;
+ ScheduledServerThreadTaskQueues.POST_CHUNK_LOAD_JOIN_TASK_MAX_DELAY = this.postChunkLoadJoin >= 0 ? this.postChunkLoadJoin : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY;
+ ScheduledServerThreadTaskQueues.ANTI_XRAY_MODIFY_BLOCKS_TASK_MAX_DELAY = this.antiXrayModifyBlocks >= 0 ? this.antiXrayModifyBlocks : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY;
+ ScheduledServerThreadTaskQueues.TELEPORT_ASYNC_TASK_MAX_DELAY = this.teleportAsync >= 0 ? this.teleportAsync : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY;
+ ScheduledServerThreadTaskQueues.SEND_COMMAND_COMPLETION_SUGGESTIONS_TASK_MAX_DELAY = this.sendCommandCompletionSuggestions >= 0 ? this.sendCommandCompletionSuggestions : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY;
+ ScheduledServerThreadTaskQueues.KICK_FOR_COMMAND_PACKET_SPAM_TASK_MAX_DELAY = this.kickForCommandPacketSpam >= 0 ? this.kickForCommandPacketSpam : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY;
+ ScheduledServerThreadTaskQueues.KICK_FOR_RECIPE_PACKET_SPAM_TASK_MAX_DELAY = this.kickForRecipePacketSpam >= 0 ? this.kickForRecipePacketSpam : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY;
+ ScheduledServerThreadTaskQueues.KICK_FOR_BOOK_TOO_LARGE_PACKET_TASK_MAX_DELAY = this.kickForBookTooLargePacket >= 0 ? this.kickForBookTooLargePacket : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY;
+ ScheduledServerThreadTaskQueues.KICK_FOR_EDITING_BOOK_TOO_QUICKLY_TASK_MAX_DELAY = this.kickForEditingBookTooQuickly >= 0 ? this.kickForEditingBookTooQuickly : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY;
+ ScheduledServerThreadTaskQueues.KICK_FOR_ILLEGAL_CHARACTERS_IN_CHAT_PACKET_TASK_MAX_DELAY = this.kickForIllegalCharactersInChatPacket >= 0 ? this.kickForIllegalCharactersInChatPacket : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY;
+ ScheduledServerThreadTaskQueues.KICK_FOR_OUT_OF_ORDER_CHAT_PACKET_TASK_MAX_DELAY = this.kickForOutOfOrderChatPacket >= 0 ? this.kickForOutOfOrderChatPacket : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY;
+ ScheduledServerThreadTaskQueues.HANDLE_DISCONNECT_TASK_MAX_DELAY = this.handleDisconnect >= 0 ? this.handleDisconnect : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY;
+ // Change the length of the pendingRunnables array of queues
+ int maxDelay = 0;
+ for (int delay : new int[]{
+ ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY,
+ ScheduledServerThreadTaskQueues.COMPLETE_CHUNK_FUTURE_TASK_MAX_DELAY,
+ ScheduledServerThreadTaskQueues.POST_CHUNK_LOAD_JOIN_TASK_MAX_DELAY,
+ ScheduledServerThreadTaskQueues.ANTI_XRAY_MODIFY_BLOCKS_TASK_MAX_DELAY,
+ ScheduledServerThreadTaskQueues.TELEPORT_ASYNC_TASK_MAX_DELAY,
+ ScheduledServerThreadTaskQueues.SEND_COMMAND_COMPLETION_SUGGESTIONS_TASK_MAX_DELAY,
+ ScheduledServerThreadTaskQueues.KICK_FOR_COMMAND_PACKET_SPAM_TASK_MAX_DELAY,
+ ScheduledServerThreadTaskQueues.KICK_FOR_RECIPE_PACKET_SPAM_TASK_MAX_DELAY,
+ ScheduledServerThreadTaskQueues.KICK_FOR_BOOK_TOO_LARGE_PACKET_TASK_MAX_DELAY,
+ ScheduledServerThreadTaskQueues.KICK_FOR_EDITING_BOOK_TOO_QUICKLY_TASK_MAX_DELAY,
+ ScheduledServerThreadTaskQueues.KICK_FOR_ILLEGAL_CHARACTERS_IN_CHAT_PACKET_TASK_MAX_DELAY,
+ ScheduledServerThreadTaskQueues.KICK_FOR_OUT_OF_ORDER_CHAT_PACKET_TASK_MAX_DELAY,
+ ScheduledServerThreadTaskQueues.HANDLE_DISCONNECT_TASK_MAX_DELAY
+ }) {
+ if (delay > maxDelay) {
+ maxDelay = delay;
+ }
+ }
+ int newPendingRunnablesLength = maxDelay + 1;
+ int oldPendingRunnablesLength = ScheduledServerThreadTaskQueues.queues.length;
+ if (oldPendingRunnablesLength != newPendingRunnablesLength) {
+ if (oldPendingRunnablesLength > newPendingRunnablesLength) {
+ // Move all tasks in queues that will be removed to the last queue
+ for (int i = newPendingRunnablesLength + 1; i < ScheduledServerThreadTaskQueues.queues.length; i++) {
+ ScheduledServerThreadTaskQueues.queues[maxDelay].addAll(ScheduledServerThreadTaskQueues.queues[i]);
+ }
+ // Update the first queue with elements index
+ if (ScheduledServerThreadTaskQueues.firstQueueWithPotentialTasksIndex >= newPendingRunnablesLength) {
+ ScheduledServerThreadTaskQueues.firstQueueWithPotentialTasksIndex = maxDelay;
+ }
+ }
+ ScheduledServerThreadTaskQueues.queues = Arrays.copyOf(ScheduledServerThreadTaskQueues.queues, newPendingRunnablesLength);
+ if (newPendingRunnablesLength > oldPendingRunnablesLength) {
+ // Create new queues
+ for (int i = oldPendingRunnablesLength; i < newPendingRunnablesLength; i++) {
+ ScheduledServerThreadTaskQueues.queues[i] = new MultiThreadedQueue<>();
+ }
+ }
+ }
+ } finally {
+ ScheduledServerThreadTaskQueues.writeLock.unlock();
+ }
+ }
+
+ }
+ // Gale end - base thread pools
+
}
public Misc misc;
diff --git a/src/main/java/org/galemc/gale/executor/AbstractBlockableEventLoop.java b/src/main/java/org/galemc/gale/executor/AbstractBlockableEventLoop.java
new file mode 100644
index 0000000000000000000000000000000000000000..e73bf57a9777488dc00efe671cee955e6fb108a1
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/AbstractBlockableEventLoop.java
@@ -0,0 +1,20 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor;
+
+import net.minecraft.util.thread.BlockableEventLoop;
+
+import java.util.concurrent.Executor;
+
+/**
+ * An interface for the common functionality of {@link BlockableEventLoop} and {@link MinecraftServerBlockableEventLoop}.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+public interface AbstractBlockableEventLoop extends Executor {
+
+ boolean isSameThread();
+
+ void executeIfPossible(Runnable runnable);
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/MinecraftServerBlockableEventLoop.java b/src/main/java/org/galemc/gale/executor/MinecraftServerBlockableEventLoop.java
new file mode 100644
index 0000000000000000000000000000000000000000..f249ef5a23e85770db224ed0b6f27598e78c5746
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/MinecraftServerBlockableEventLoop.java
@@ -0,0 +1,188 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor;
+
+import com.mojang.logging.LogUtils;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.util.thread.BlockableEventLoop;
+import net.minecraft.util.thread.ProcessorHandle;
+import net.minecraft.util.thread.ReentrantBlockableEventLoop;
+import org.galemc.gale.executor.annotation.thread.ServerThreadOnly;
+import org.galemc.gale.executor.queue.BaseTaskQueues;
+import org.galemc.gale.executor.queue.ScheduledServerThreadTaskQueues;
+import org.galemc.gale.executor.thread.pooled.ServerThread;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+import org.spigotmc.WatchdogThread;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.function.BooleanSupplier;
+import java.util.function.Supplier;
+
+/**
+ * This is a base class for {@link MinecraftServer}, as a replacement of {@link BlockableEventLoop}
+ * (and the intermediary class {@link ReentrantBlockableEventLoop}.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+public class MinecraftServerBlockableEventLoop implements ProcessorHandle<Runnable>, 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 <V> CompletableFuture<V> submit(Supplier<V> task) {
+ return scheduleExecutables() ? CompletableFuture.supplyAsync(task, this) : CompletableFuture.completedFuture(task.get());
+ }
+
+ private CompletableFuture<Void> submitAsync(Runnable runnable) {
+ return CompletableFuture.supplyAsync(() -> {
+ runnable.run();
+ return null;
+ }, this);
+ }
+
+ public CompletableFuture<Void> submit(Runnable task) {
+ if (scheduleExecutables()) {
+ return this.submitAsync(task);
+ } else {
+ task.run();
+ return CompletableFuture.completedFuture(null);
+ }
+ }
+
+ public void executeBlocking(Runnable runnable) {
+ if (Thread.currentThread() != ServerThread.getInstance()) {
+ this.submitAsync(runnable).join();
+ } else {
+ runnable.run();
+ }
+ }
+
+ /**
+ * @deprecated Use {@link ScheduledServerThreadTaskQueues#add(Runnable, int)} instead:
+ * do not rely on {@link ScheduledServerThreadTaskQueues#DEFAULT_TASK_MAX_DELAY}.
+ */
+ @Deprecated
+ @Override
+ public void tell(@NotNull Runnable message) {
+ ScheduledServerThreadTaskQueues.add(() -> {
+ //noinspection NonAtomicOperationOnVolatileField
+ ++reentrantCount;
+ try {
+ message.run();
+ } catch (Exception var3) {
+ if (var3.getCause() instanceof ThreadDeath) throw var3; // Paper
+ LOGGER.error(LogUtils.FATAL_MARKER, "Error executing task on {}", NAME, var3);
+ } finally {
+ //noinspection NonAtomicOperationOnVolatileField
+ --reentrantCount;
+ if (MinecraftServer.isWaitingUntilNextTick) {
+ MinecraftServer.signalServerThreadIfCurrentManagedBlockStopConditionBecameTrue();
+ }
+ }
+ MinecraftServer.SERVER.executeMidTickTasks(); // Paper - execute chunk tasks mid tick
+ });
+ }
+
+ @Override
+ public void execute(@NotNull Runnable var1) {
+ if (scheduleExecutables()) {
+ this.tell(var1);
+ } else {
+ var1.run();
+ }
+ }
+
+ @Override
+ public boolean isSameThread() {
+ return Thread.currentThread() == MinecraftServer.serverThread;
+ }
+
+ @Override
+ public void executeIfPossible(Runnable runnable) {
+ if (MinecraftServer.SERVER.isStopped()) {
+ throw new RejectedExecutionException("Server already shutting down");
+ } else {
+ this.execute(runnable);
+ }
+ }
+
+ /**
+ * Runs all tasks, regardless of which tick they must be finished in, or whether there is time.
+ */
+ @ServerThreadOnly
+ protected void runAllMainThreadTasksForAllTicks() {
+ Runnable task;
+ while (true) {
+ // Force polling every tasks regardless of the tick they have to be finished by
+ MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking = false;
+ task = ScheduledServerThreadTaskQueues.poll(ServerThread.getInstance(), true);
+ if (task == null) {
+ break;
+ }
+ task.run();
+ }
+ }
+
+ /**
+ * Runs all tasks while there is time.
+ * Runs at least all tasks that must be finished in the current tick, regardless of whether there is time.
+ */
+ @ServerThreadOnly
+ protected void runAllTasksWithinTimeOrForCurrentTick() {
+ Runnable task;
+ while (true) {
+ /*
+ Update this value accurately: we are in 'spare time' here, we may have more time or not, and we are
+ definitely not already blocking.
+ */
+ MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking = !MinecraftServer.SERVER.haveTime();
+ task = BaseTaskQueues.anyTickScheduledServerThread.poll(ServerThread.getInstance());
+ if (task == null) {
+ break;
+ }
+ task.run();
+ }
+ }
+
+ @ServerThreadOnly
+ public void managedBlock(@NotNull BooleanSupplier stopCondition) {
+ MinecraftServer.currentManagedBlockStopCondition = stopCondition;
+ try {
+ // Check stop condition beforehand to prevent unnecessarily releasing main thread
+ MinecraftServer.currentManagedBlockStopConditionHasBecomeTrue = false;
+ if (stopCondition.getAsBoolean()) {
+ MinecraftServer.currentManagedBlockStopConditionHasBecomeTrue = true;
+ return;
+ }
+ //noinspection NonAtomicOperationOnVolatileField
+ ++blockingCount;
+ try {
+ MinecraftServer.serverThread.runTasksUntil(stopCondition, null);
+ } finally {
+ //noinspection NonAtomicOperationOnVolatileField
+ --blockingCount;
+ }
+ } finally {
+ MinecraftServer.currentManagedBlockStopCondition = null;
+ }
+ }
+
+ @Override
+ public @NotNull String name() {
+ return NAME;
+ }
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/annotation/PotentiallyBlocking.java b/src/main/java/org/galemc/gale/executor/annotation/PotentiallyBlocking.java
index 71f26852c96dea34ea07efe07f834f8262509957..d324c303245bcbedaaaab573803d73caff941901 100644
--- a/src/main/java/org/galemc/gale/executor/annotation/PotentiallyBlocking.java
+++ b/src/main/java/org/galemc/gale/executor/annotation/PotentiallyBlocking.java
@@ -2,7 +2,7 @@
package org.galemc.gale.executor.annotation;
-import org.galemc.gale.executor.thread.BaseThread;
+import org.galemc.gale.executor.thread.AbstractYieldingThread;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
@@ -19,7 +19,7 @@ import java.lang.annotation.Target;
* {@link PotentiallyBlocking}, {@link PotentiallyYielding} or {@link YieldFree} may all be used.
* <br>
* Methods that are potentially blocking, including those annotated with {@link PotentiallyBlocking}, must never
- * be called on a {@link BaseThread}.
+ * be called on an {@link AbstractYieldingThread}.
*
* @author Martijn Muijsers under AGPL-3.0
*/
diff --git a/src/main/java/org/galemc/gale/executor/annotation/PotentiallyYielding.java b/src/main/java/org/galemc/gale/executor/annotation/PotentiallyYielding.java
index e87ee2612348fc559b21256cc7cadfc684f01f8e..7ff4e4ab43d316e319efb33b2dd365d679a58118 100644
--- a/src/main/java/org/galemc/gale/executor/annotation/PotentiallyYielding.java
+++ b/src/main/java/org/galemc/gale/executor/annotation/PotentiallyYielding.java
@@ -2,6 +2,9 @@
package org.galemc.gale.executor.annotation;
+import org.galemc.gale.executor.lock.YieldingLock;
+import org.galemc.gale.executor.thread.AbstractYieldingThread;
+
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@@ -16,6 +19,9 @@ import java.lang.annotation.Target;
* <br>
* In a method annotated with {@link PotentiallyYielding}, the only methods that can be called are those
* annotated with {@link PotentiallyYielding} or {@link YieldFree}.
+ * <br>
+ * It should be assumed that any method annotated with {@link PotentiallyYielding} is potentially blocking if used
+ * on a thread that is not a {@link AbstractYieldingThread}.
*
* @author Martijn Muijsers under AGPL-3.0
*/
diff --git a/src/main/java/org/galemc/gale/executor/annotation/thread/AsyncThreadOnly.java b/src/main/java/org/galemc/gale/executor/annotation/thread/AsyncThreadOnly.java
new file mode 100644
index 0000000000000000000000000000000000000000..604ece0c20e986afdf6958ba969052b2c69762b7
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/annotation/thread/AsyncThreadOnly.java
@@ -0,0 +1,36 @@
+// Gale - thread-safety annotations
+
+package org.galemc.gale.executor.annotation.thread;
+
+import org.galemc.gale.executor.annotation.Access;
+import org.galemc.gale.executor.annotation.PotentiallyBlocking;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+/**
+ * An annotation primarily for methods, identifying methods that can only be called on a thread that is an instance
+ * of {@link AsyncThread}.
+ * <br>
+ * This annotation can also be used on fields or classes, similar to {@link ThreadRestricted}.
+ * <br>
+ * In a method annotated with {@link AsyncThreadOnly}, fields and methods annotated with
+ * {@link AsyncThreadOnly}, {@link BaseYieldingThreadOnly}, {@link YieldingThreadOnly}
+ * or {@link AnyThreadSafe} may be used.
+ * <br>
+ * Methods that are annotated with {@link AsyncThreadOnly} must never call methods that are annotated with
+ * {@link PotentiallyBlocking}.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+@Documented
+@Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD})
+public @interface AsyncThreadOnly {
+
+ /**
+ * @see ThreadRestricted#fieldAccess()
+ */
+ Access value() default Access.READ_WRITE;
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/annotation/thread/BaseYieldingThreadOnly.java b/src/main/java/org/galemc/gale/executor/annotation/thread/BaseYieldingThreadOnly.java
new file mode 100644
index 0000000000000000000000000000000000000000..2b20fd3fd5e9d73049bcc6bf0cbca0eb7a77630b
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/annotation/thread/BaseYieldingThreadOnly.java
@@ -0,0 +1,36 @@
+// Gale - thread-safety annotations
+
+package org.galemc.gale.executor.annotation.thread;
+
+import org.galemc.gale.executor.annotation.Access;
+import org.galemc.gale.executor.annotation.PotentiallyBlocking;
+import org.galemc.gale.executor.thread.BaseYieldingThread;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+/**
+ * An annotation primarily for methods, identifying methods that can only be called on a thread that is an instance
+ * of {@link BaseYieldingThread}.
+ * <br>
+ * This annotation can also be used on fields or classes, similar to {@link ThreadRestricted}.
+ * <br>
+ * In a method annotated with {@link BaseYieldingThreadOnly}, fields and methods annotated with
+ * {@link BaseYieldingThreadOnly}, {@link YieldingThreadOnly} or {@link AnyThreadSafe} may be used.
+ * <br>
+ * Methods that are annotated with {@link BaseYieldingThreadOnly} must never call methods that are annotated with
+ * {@link PotentiallyBlocking}.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+@Documented
+@Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD})
+public @interface BaseYieldingThreadOnly {
+
+ /**
+ * @see ThreadRestricted#fieldAccess()
+ */
+ Access value() default Access.READ_WRITE;
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/annotation/thread/OriginalServerThreadOnly.java b/src/main/java/org/galemc/gale/executor/annotation/thread/OriginalServerThreadOnly.java
new file mode 100644
index 0000000000000000000000000000000000000000..13bd7088eb33cfb8e839debcb77f3a26c2d2a441
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/annotation/thread/OriginalServerThreadOnly.java
@@ -0,0 +1,35 @@
+// Gale - thread-safety annotations
+
+package org.galemc.gale.executor.annotation.thread;
+
+import org.galemc.gale.executor.annotation.Access;
+import org.galemc.gale.executor.annotation.PotentiallyBlocking;
+import org.galemc.gale.executor.thread.OriginalServerThread;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+/**
+ * An annotation primarily for methods, identifying methods that can only be called from the
+ * {@link OriginalServerThread}.
+ * <br>
+ * This annotation can also be used on fields, similar to {@link ThreadRestricted}.
+ * <br>
+ * Methods that are annotated with {@link OriginalServerThreadOnly} must never call methods that are annotated with
+ * {@link PotentiallyBlocking}.
+ *
+ * @see ThreadRestricted
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+@Documented
+@Target({ElementType.METHOD, ElementType.FIELD})
+public @interface OriginalServerThreadOnly {
+
+ /**
+ * @see ThreadRestricted#fieldAccess()
+ */
+ Access value() default Access.READ_WRITE;
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/annotation/thread/ServerThreadOnly.java b/src/main/java/org/galemc/gale/executor/annotation/thread/ServerThreadOnly.java
new file mode 100644
index 0000000000000000000000000000000000000000..efd540e2a40d79f70e7ac6a709bb10f0138ed7a8
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/annotation/thread/ServerThreadOnly.java
@@ -0,0 +1,38 @@
+// Gale - thread-safety annotations
+
+package org.galemc.gale.executor.annotation.thread;
+
+import org.galemc.gale.executor.annotation.Access;
+import org.galemc.gale.executor.annotation.PotentiallyBlocking;
+import org.galemc.gale.executor.thread.pooled.ServerThread;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+/**
+ * An annotation primarily for methods, identifying methods that can only be called from a {@link ServerThread}.
+ * <br>
+ * This annotation can also be used on fields or classes, similar to {@link ThreadRestricted}.
+ * <br>
+ * In a method annotated with {@link ServerThreadOnly}, fields and methods annotated with
+ * {@link ServerThreadOnly}, {@link BaseYieldingThreadOnly}, {@link YieldingThreadOnly}
+ * or {@link AnyThreadSafe} may be used.
+ * <br>
+ * Methods that are annotated with {@link ServerThreadOnly} must never call methods that are annotated with
+ * {@link PotentiallyBlocking}.
+ *
+ * @see ThreadRestricted
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+@Documented
+@Target({ElementType.METHOD, ElementType.FIELD})
+public @interface ServerThreadOnly {
+
+ /**
+ * @see ThreadRestricted#fieldAccess()
+ */
+ Access value() default Access.READ_WRITE;
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/annotation/thread/TickAssistThreadOnly.java b/src/main/java/org/galemc/gale/executor/annotation/thread/TickAssistThreadOnly.java
new file mode 100644
index 0000000000000000000000000000000000000000..b86526dbdd531827fc4064b22c3281b1215aa188
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/annotation/thread/TickAssistThreadOnly.java
@@ -0,0 +1,37 @@
+// Gale - thread-safety annotations
+
+package org.galemc.gale.executor.annotation.thread;
+
+import org.galemc.gale.executor.annotation.Access;
+import org.galemc.gale.executor.annotation.PotentiallyBlocking;
+import org.galemc.gale.executor.thread.pooled.TickAssistThread;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+/**
+ * An annotation primarily for methods, identifying methods that can only be called on a thread that is an instance
+ * of {@link TickAssistThread}.
+ * <br>
+ * This annotation can also be used on fields or classes, similar to {@link ThreadRestricted}.
+ * <br>
+ * In a method annotated with {@link TickAssistThreadOnly}, fields and methods annotated with
+ * {@link TickAssistThreadOnly}, {@link BaseYieldingThreadOnly}, {@link YieldingThreadOnly}
+ * or {@link AnyThreadSafe} may be used.
+ * <br>
+ * Methods that are annotated with {@link TickAssistThreadOnly} must never call methods that are annotated with
+ * {@link PotentiallyBlocking}.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+@Documented
+@Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD})
+public @interface TickAssistThreadOnly {
+
+ /**
+ * @see ThreadRestricted#fieldAccess()
+ */
+ Access value() default Access.READ_WRITE;
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/annotation/thread/YieldingThreadOnly.java b/src/main/java/org/galemc/gale/executor/annotation/thread/YieldingThreadOnly.java
new file mode 100644
index 0000000000000000000000000000000000000000..e37018a92d8854674b12172af33073e39982b3a6
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/annotation/thread/YieldingThreadOnly.java
@@ -0,0 +1,36 @@
+// Gale - thread-safety annotations
+
+package org.galemc.gale.executor.annotation.thread;
+
+import org.galemc.gale.executor.annotation.Access;
+import org.galemc.gale.executor.annotation.PotentiallyBlocking;
+import org.galemc.gale.executor.thread.AbstractYieldingThread;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+/**
+ * An annotation primarily for methods, identifying methods that can only be called on a thread that is an instance
+ * of {@link AbstractYieldingThread}.
+ * <br>
+ * This annotation can also be used on fields or classes, similar to {@link ThreadRestricted}.
+ * <br>
+ * In a method annotated with {@link YieldingThreadOnly}, fields and methods annotated with
+ * {@link YieldingThreadOnly} or {@link AnyThreadSafe} may be used.
+ * <br>
+ * Methods that are annotated with {@link YieldingThreadOnly} must never call methods that are annotated with
+ * {@link PotentiallyBlocking}.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+@Documented
+@Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD})
+public @interface YieldingThreadOnly {
+
+ /**
+ * @see ThreadRestricted#fieldAccess()
+ */
+ Access value() default Access.READ_WRITE;
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/lock/MultipleWaitingBaseThreadsYieldingLock.java b/src/main/java/org/galemc/gale/executor/lock/MultipleWaitingBaseThreadsYieldingLock.java
new file mode 100644
index 0000000000000000000000000000000000000000..377e41518a336d4efaf33e3e4257229225761627
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/lock/MultipleWaitingBaseThreadsYieldingLock.java
@@ -0,0 +1,42 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.lock;
+
+import org.galemc.gale.executor.thread.BaseYieldingThread;
+import org.galemc.gale.executor.thread.wait.SignalReason;
+import org.galemc.gale.executor.thread.wait.WaitingBaseThreadSet;
+import org.galemc.gale.executor.thread.wait.WaitingThreadSet;
+
+import java.util.concurrent.locks.Lock;
+
+/**
+ * A {@link YieldingLock} for which multiple {@link BaseYieldingThread}s may be waiting at the same time.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+public class MultipleWaitingBaseThreadsYieldingLock extends YieldingLock {
+
+ private final WaitingThreadSet waitingThreads = new WaitingBaseThreadSet();
+
+ private final SignalReason signalReason = SignalReason.createForWaitingThreadSet(waitingThreads);
+
+ public MultipleWaitingBaseThreadsYieldingLock(Lock innerLock) {
+ super(innerLock);
+ }
+
+ @Override
+ public void addWaitingThread(Thread thread) {
+ this.waitingThreads.add(thread);
+ }
+
+ @Override
+ public void removeWaitingThread(Thread thread) {
+ this.waitingThreads.remove(thread);
+ }
+
+ @Override
+ public SignalReason getSignalReason() {
+ return this.signalReason;
+ }
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/lock/YieldingLock.java b/src/main/java/org/galemc/gale/executor/lock/YieldingLock.java
new file mode 100644
index 0000000000000000000000000000000000000000..1977fb5fb3403a8ecd8e1396bcd1244eb27e78f8
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/lock/YieldingLock.java
@@ -0,0 +1,122 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.lock;
+
+import org.galemc.gale.executor.annotation.thread.AnyThreadSafe;
+import org.galemc.gale.executor.annotation.PotentiallyYielding;
+import org.galemc.gale.executor.annotation.YieldFree;
+import org.galemc.gale.executor.thread.AbstractYieldingThread;
+import org.galemc.gale.executor.thread.wait.SignalReason;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+
+/**
+ * A wrapper for a lock that can be acquired, but if not able to be acquired right away, can cause the current thread
+ * to perform other tasks, attempting to acquire the lock again at a later time.
+ * <br>
+ * The lock is reentrant if the underlying controlled lock is.
+ * <br>
+ * The lock only be speculatively acquired from any {@link AbstractYieldingThread}.
+ * Acquiring it on a thread that is not an {@link AbstractYieldingThread} will perform regular locking
+ * on the underlying controlled lock, which typically blocks the thread.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+@AnyThreadSafe
+public abstract class YieldingLock implements Lock {
+
+ private final Lock innerLock;
+
+ public YieldingLock(Lock innerLock) {
+ this.innerLock = innerLock;
+ }
+
+ /**
+ * Attempts to acquire the lock immediately.
+ *
+ * @return Whether the lock was acquired.
+ */
+ @YieldFree
+ @Override
+ public boolean tryLock() {
+ return innerLock.tryLock();
+ }
+
+ /**
+ * Acquires the lock.
+ * <br>
+ * If the current threads is an {@link AbstractYieldingThread},
+ * this will yield to other tasks while the lock can not be acquired.
+ * Otherwise, this will block until the lock is acquired.
+ */
+ @PotentiallyYielding
+ @Override
+ public void lock() {
+ // Try to acquire the lock straight away
+ if (!this.innerLock.tryLock()) {
+ // If unsuccessful, we find out our current thread
+ AbstractYieldingThread yieldingThread = AbstractYieldingThread.currentYieldingThread();
+ // If we are not on a yielding thread, we wait for the lock instead of yielding
+ if (yieldingThread == null) {
+ this.innerLock.lock();
+ return;
+ }
+ // Otherwise, we yield to other tasks until the lock can be acquired
+ yieldingThread.yieldUntil(null, this);
+ }
+ }
+
+ /**
+ * Releases the lock (must be called after having completed the computation block that required the lock).
+ */
+ @Override
+ public void unlock() {
+ this.innerLock.unlock();
+ // Signal the first waiting thread, if any.
+ // Another thread could also acquire the lock at this moment, so when we signal the thread we obtain below,
+ // it may already be too late for the polled thread to acquire this lock
+ // (but note that the same thread cannot have been added again because only the thread itself can do that -
+ // and it is still waiting).
+ this.getSignalReason().signalAnother();
+ }
+
+ @Override
+ public boolean tryLock(long l, @NotNull TimeUnit timeUnit) throws InterruptedException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void lockInterruptibly() throws InterruptedException {
+ throw new UnsupportedOperationException();
+ }
+
+ @NotNull
+ @Override
+ public Condition newCondition() {
+ // The inner lock may itself not support newCondition and throw UnsupportedOperationException
+ return this.innerLock.newCondition();
+ }
+
+ /**
+ * Adds a thread to the set of threads waiting for this lock to be released.
+ *
+ * @param thread The thread to register as waiting for this lock.
+ */
+ @YieldFree
+ public abstract void addWaitingThread(Thread thread);
+
+ /**
+ * Removes a thread from the set of threads waiting for this lock to be released.
+ *
+ * @param thread The thread to unregister as waiting for this lock.
+ */
+ @YieldFree
+ public abstract void removeWaitingThread(Thread thread);
+
+ @YieldFree
+ public abstract SignalReason getSignalReason();
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/queue/AbstractTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/AbstractTaskQueue.java
new file mode 100644
index 0000000000000000000000000000000000000000..b38bf6c1da417f00a3c8e2ebb0d2a6df13696921
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/queue/AbstractTaskQueue.java
@@ -0,0 +1,104 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.queue;
+
+import org.galemc.gale.executor.annotation.thread.AnyThreadSafe;
+import org.galemc.gale.executor.annotation.YieldFree;
+import org.galemc.gale.executor.annotation.thread.BaseYieldingThreadOnly;
+import org.galemc.gale.executor.thread.BaseYieldingThread;
+import org.galemc.gale.executor.thread.pooled.AbstractYieldingSignallableThreadPool;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * An interface for a task queue that may contain tasks that are potentially yielding and tasks that are yield-free.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+@AnyThreadSafe
+@YieldFree
+public interface AbstractTaskQueue {
+
+ /**
+ * @return A name that the queue can be identified by.
+ */
+ String getName();
+
+ /**
+ * @return Whether this queue has potentially yielding tasks that could start right now.
+ *
+ * @see #hasTasksThatCanStartNow
+ */
+ boolean hasYieldingTasksThatCanStartNow();
+
+ /**
+ * @return Whether this queue has yield-free tasks that could start right now.
+ *
+ * @see #hasTasksThatCanStartNow
+ */
+ boolean hasFreeTasksThatCanStartNow();
+
+ /**
+ * @return Whether this queue has tasks that could start right now. An example of
+ * tasks that could not start right now are tasks in {@link ScheduledServerThreadTaskQueues} that are scheduled for
+ * a later tick, while we are already out of spare time this tick.
+ */
+ @AnyThreadSafe
+ default boolean hasTasksThatCanStartNow(BaseYieldingThread thread) {
+ return (!thread.isRestrictedDueToYieldDepth && this.hasYieldingTasksThatCanStartNow()) || this.hasFreeTasksThatCanStartNow();
+ }
+
+ /**
+ * @return Whether this queue has any tasks at all.
+ */
+ boolean hasTasks();
+
+ /**
+ * Attempts to poll a task.
+ *
+ * @param currentThread The current thread.
+ * @return The polled task, or null if this queue was empty.
+ */
+ @BaseYieldingThreadOnly
+ @Nullable Runnable poll(BaseYieldingThread currentThread);
+
+ /**
+ * Schedules a new task to this queue.
+ *
+ * @param task The task to schedule.
+ * @param yielding Whether the task is potentially yielding.
+ */
+ void add(Runnable task, boolean yielding);
+
+ /**
+ * Sets the thread pool of this queue. This ensures queues do not accidentally initialize the thread pools
+ * before they have finished initializing themselves.
+ */
+ void setThreadPool(AbstractYieldingSignallableThreadPool threadPool);
+
+ /**
+ * @return Whether any of the given task queues is non-empty.
+ */
+ static boolean taskQueuesHaveTasks(AbstractTaskQueue[] queues) {
+ for (AbstractTaskQueue queue : queues) {
+ if (queue.hasTasks()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @return Whether any of the given task queues has a task that the given thread could start
+ * based on amongst others whether {@link BaseYieldingThread#isRestrictedDueToYieldDepth} is true,
+ * and potentially other external circumstances that may stop a queue from releasing some its held tasks.
+ */
+ static boolean taskQueuesHaveTasksCouldStart(AbstractTaskQueue[] queues, BaseYieldingThread thread) {
+ for (AbstractTaskQueue queue : queues) {
+ if (queue.hasTasksThatCanStartNow(thread)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledChunkCacheTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledChunkCacheTaskQueue.java
new file mode 100644
index 0000000000000000000000000000000000000000..c6672e91706cdeded2f5f43c1b0c4d8dbe2df75e
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledChunkCacheTaskQueue.java
@@ -0,0 +1,54 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.queue;
+
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerChunkCache;
+import net.minecraft.server.level.ServerLevel;
+import org.galemc.gale.executor.annotation.thread.AnyThreadSafe;
+import org.galemc.gale.executor.annotation.YieldFree;
+import org.galemc.gale.executor.thread.pooled.AbstractYieldingSignallableThreadPool;
+import org.galemc.gale.executor.thread.pooled.ServerThread;
+import org.galemc.gale.executor.thread.pooled.ServerThreadPool;
+import org.galemc.gale.executor.thread.wait.SignalReason;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * This class provides access to, but does not store, the tasks scheduled to be executed on the main thread,
+ * that are scheduled and normally polled by each world's {@link ServerChunkCache#mainThreadProcessor} in their
+ * respective {@link ServerChunkCache.MainThreadExecutor#managedBlock}. These tasks could normally also be run in the
+ * server's {@link MinecraftServer#managedBlock} if there were no more global scheduled server thread tasks, and as
+ * such we provide access to polling these tasks from a {@link ServerThread}.
+ * <br>
+ * All tasks provided by this queue must be yield-free.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+@AnyThreadSafe
+@YieldFree
+public final class AllLevelsScheduledChunkCacheTaskQueue extends AllLevelsScheduledTaskQueue {
+
+ AllLevelsScheduledChunkCacheTaskQueue() {
+ super();
+ }
+
+ @Override
+ public String getName() {
+ return "AllLevelsScheduledChunkCache";
+ }
+
+ @Override
+ protected boolean hasLevelTasks(ServerLevel level) {
+ return level.getChunkSource().mainThreadProcessor.hasPendingTasks();
+ }
+
+ @Override
+ protected @Nullable Runnable pollLevel(ServerLevel level) {
+ var executor = level.getChunkSource().mainThreadProcessor;
+ if (executor.hasPendingTasks()) {
+ return executor::pollTask;
+ }
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledTaskQueue.java
new file mode 100644
index 0000000000000000000000000000000000000000..158aa20f2840260306ecd4c48358544384fbf285
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledTaskQueue.java
@@ -0,0 +1,103 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.queue;
+
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerLevel;
+import org.galemc.gale.executor.annotation.thread.AnyThreadSafe;
+import org.galemc.gale.executor.annotation.YieldFree;
+import org.galemc.gale.executor.thread.BaseYieldingThread;
+import org.galemc.gale.executor.thread.pooled.AbstractYieldingSignallableThreadPool;
+import org.galemc.gale.executor.thread.wait.SignalReason;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Common implementation for {@link AllLevelsScheduledChunkCacheTaskQueue} and
+ * {@link AllLevelsScheduledTickThreadChunkTaskQueue}.
+ * <br>
+ * All tasks provided by this queue must be yield-free.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+@AnyThreadSafe
+@YieldFree
+public abstract class AllLevelsScheduledTaskQueue implements AbstractTaskQueue {
+
+ /**
+ * Will be initialized in {@link #setThreadPool}.
+ */
+ public SignalReason signalReason;
+
+ /**
+ * An iteration index for iterating over the levels in {@link #poll}.
+ */
+ private int levelIterationIndex;
+
+ protected AllLevelsScheduledTaskQueue() {}
+
+ protected abstract boolean hasLevelTasks(ServerLevel level);
+
+ protected abstract @Nullable Runnable pollLevel(ServerLevel level);
+
+ @Override
+ public boolean hasYieldingTasksThatCanStartNow() {
+ return false;
+ }
+
+ @Override
+ public boolean hasFreeTasksThatCanStartNow() {
+ // Skip during server bootstrap or if there is no more time in the current spare time
+ if (MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking || !MinecraftServer.isConstructed) {
+ return false;
+ }
+ return this.hasTasks();
+ }
+
+ @Override
+ public boolean hasTasks() {
+ for (ServerLevel level : MinecraftServer.SERVER.getAllLevels()) {
+ if (this.hasLevelTasks(level)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public @Nullable Runnable poll(BaseYieldingThread currentThread) {
+ // Skip during server bootstrap or if there is no more time in the current spare time
+ if (MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking || !MinecraftServer.isConstructed) {
+ return null;
+ }
+ ServerLevel[] levels = MinecraftServer.SERVER.getAllLevelsArray();
+ int startIndex = this.levelIterationIndex = Math.min(this.levelIterationIndex, levels.length - 1);
+ // Paper - force execution of all worlds, do not just bias the first
+ do {
+ ServerLevel level = levels[this.levelIterationIndex++];
+ if (this.levelIterationIndex == levels.length) {
+ this.levelIterationIndex = 0;
+ }
+ if (level.serverLevelArrayIndex != -1) {
+ Runnable task = this.pollLevel(level);
+ if (task != null) {
+ return task;
+ }
+ }
+ } while (this.levelIterationIndex != startIndex);
+ return null;
+ }
+
+ @Override
+ public void add(Runnable task, boolean yielding) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setThreadPool(AbstractYieldingSignallableThreadPool threadPool) {
+ if (this.signalReason != null) {
+ throw new IllegalStateException(this.getClass().getSimpleName() + ".signalReason was already initialized");
+ }
+ this.signalReason = SignalReason.createForThreadPoolNewTasks(threadPool, false);
+ }
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledTickThreadChunkTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledTickThreadChunkTaskQueue.java
new file mode 100644
index 0000000000000000000000000000000000000000..dd9f10bbcdfe9b2e5279fb34a913c967d7069fa7
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledTickThreadChunkTaskQueue.java
@@ -0,0 +1,53 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.queue;
+
+import io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler;
+import io.papermc.paper.util.TickThread;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerChunkCache;
+import net.minecraft.server.level.ServerLevel;
+import org.galemc.gale.executor.annotation.thread.AnyThreadSafe;
+import org.galemc.gale.executor.annotation.YieldFree;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * This class provides access to, but does not store, the tasks scheduled to be executed on the main thread,
+ * that are scheduled and normally polled by each world's {@link ServerLevel#chunkTaskScheduler} using
+ * respective {@link ChunkTaskScheduler#executeMainThreadTask}. These tasks could normally also be run in the
+ * server's {@link MinecraftServer#managedBlock} or a level's {@link ServerChunkCache}'s
+ * {@link ServerChunkCache.MainThreadExecutor#managedBlock}, and as such we provide access to polling these tasks
+ * from a {@link TickThread}.
+ * <br>
+ * All tasks provided by this queue must be yield-free.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+@AnyThreadSafe
+@YieldFree
+public final class AllLevelsScheduledTickThreadChunkTaskQueue extends AllLevelsScheduledTaskQueue {
+
+ AllLevelsScheduledTickThreadChunkTaskQueue() {
+ super();
+ }
+
+ @Override
+ public String getName() {
+ return "AllLevelsScheduledTickThreadChunk";
+ }
+
+ @Override
+ protected boolean hasLevelTasks(ServerLevel level) {
+ return level.chunkTaskScheduler.mainThreadExecutor.hasScheduledUncompletedTasksVolatile();
+ }
+
+ @Override
+ protected @Nullable Runnable pollLevel(ServerLevel level) {
+ var executor = level.chunkTaskScheduler.mainThreadExecutor;
+ if (executor.hasScheduledUncompletedTasksVolatile()) {
+ return executor::executeTask;
+ }
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/queue/AnyTickScheduledServerThreadTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/AnyTickScheduledServerThreadTaskQueue.java
new file mode 100644
index 0000000000000000000000000000000000000000..82f3ae8eea503fb51f5c7b0547e71234cc169a07
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/queue/AnyTickScheduledServerThreadTaskQueue.java
@@ -0,0 +1,29 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.queue;
+
+import org.galemc.gale.executor.annotation.thread.AnyThreadSafe;
+import org.galemc.gale.executor.annotation.YieldFree;
+
+/**
+ * This class provides access to, but does not store, the tasks scheduled to be executed on the main thread,
+ * that must be finished by some time in the future, but not necessarily within the current tick or its spare time.
+ * <br>
+ * This queue does not support {@link #add}.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+@AnyThreadSafe
+@YieldFree
+public final class AnyTickScheduledServerThreadTaskQueue extends TickRequiredScheduledServerThreadTaskQueue {
+
+ AnyTickScheduledServerThreadTaskQueue() {
+ super(true);
+ }
+
+ @Override
+ public String getName() {
+ return "AnyTickScheduledServerThread";
+ }
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/queue/BaseTaskQueues.java b/src/main/java/org/galemc/gale/executor/queue/BaseTaskQueues.java
new file mode 100644
index 0000000000000000000000000000000000000000..286b893749db38afc49fd03a0ac006598268d26e
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/queue/BaseTaskQueues.java
@@ -0,0 +1,115 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.queue;
+
+import io.papermc.paper.util.MCUtil;
+import io.papermc.paper.util.TickThread;
+import org.galemc.gale.executor.thread.pooled.TickAssistThread;
+import org.galemc.gale.executor.thread.deferral.TickThreadDeferral;
+import org.galemc.gale.executor.thread.AbstractYieldingThread;
+
+/**
+ * This class statically provides a list of task queues containing tasks for {@link AbstractYieldingThread}s to poll from.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+public final class BaseTaskQueues {
+
+ private BaseTaskQueues() {}
+
+ /**
+ * This queue stores the tasks scheduled to be executed on a {@link TickThread}, that are procedures that must run
+ * on a tick thread, but their completion is currently needed by another task that has started running on a thread
+ * that was not the main thread at the time of scheduling the main-thread-only procedure.
+ * <br>
+ * 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}.
+ * <br>
+ * This queue may contain potentially yielding and yield-free tasks.
+ * <br>
+ * This queue's {@link AbstractTaskQueue#add} must not be called from the server thread,
+ * because the server thread must not defer to itself (because tasks in this queue are assumed to have to run
+ * independent of other server thread tasks, therefore this will cause a deadlock due to the scheduled task
+ * never starting).
+ * Instead, any task that must be deferred to the main thread must instead simply be executed
+ * when encountered on the main thread.
+ */
+ public static final YieldingAndFreeSimpleTaskQueue deferredToServerThread = new YieldingAndFreeSimpleTaskQueue("DeferredToServerThread", true);
+
+ /**
+ * This queue stores the tasks scheduled to be executed on a {@link TickThread}, that are procedures that must run
+ * on a tick thread, but their completion is currently needed by another task that has started running on a thread
+ * that was not the main thread at the time of scheduling the main-thread-only procedure.
+ * <br>
+ * This queue may contain potentially yielding and yield-free tasks.
+ * <br>
+ * This is currently completely unused, because {@link TickThreadDeferral} simply adds task to
+ * {@link #deferredToServerThread} instead, since there are currently no special {@link TickThread}s.
+ */
+ @SuppressWarnings("unused")
+ public static final YieldingAndFreeSimpleTaskQueue deferredToUniversalTickThread = new YieldingAndFreeSimpleTaskQueue("DeferredToUniversalTickThread", true);
+
+ /**
+ * This queue explicitly stores tasks that represent steps or parts of steps in ticking the server and that must be
+ * executed on the main thread, and as such always have a higher priority in being started than pending tasks in
+ * {@link #anyTickScheduledServerThread} and {@link #scheduledAsync}.
+ * <br>
+ * This queue may contain potentially yielding and yield-free tasks.
+ * <br>
+ * Tasks in every queue are performed in the order they are added (first-in, first-out). Note that this means
+ * not all main-thread-only tick tasks are necessarily performed in the order they are added, because they may be
+ * in different queues: either the queue for potentially yielding tasks or the queue for yield-free tasks.
+ */
+ public static final YieldingAndFreeSimpleTaskQueue serverThreadTick = new YieldingAndFreeSimpleTaskQueue("ServerThreadTick");
+
+ /**
+ * Currently unused: only {@link #anyTickScheduledServerThread} is polled.
+ *
+ * @see ThisTickScheduledServerThreadTaskQueue
+ */
+ @SuppressWarnings("unused")
+ public static final ThisTickScheduledServerThreadTaskQueue thisTickScheduledServerThread = null;
+
+ /**
+ * @see AnyTickScheduledServerThreadTaskQueue
+ */
+ public static final AnyTickScheduledServerThreadTaskQueue anyTickScheduledServerThread = new AnyTickScheduledServerThreadTaskQueue();
+
+ /**
+ * This queue explicitly stores tasks that represent steps or parts of steps in ticking the server that do not have
+ * to be executed on the main thread (but must be executed on a {@link TickAssistThread}), and have a higher priority
+ * in being started than pending tasks in {@link #scheduledAsync}.
+ * <br>
+ * This queue may contain potentially yielding and yield-free tasks.
+ * <br>
+ * Tasks in every queue are performed in the order they are added (first-in, first-out). Note that this means
+ * not all {@link TickAssistThread} tick tasks are necessarily performed in the order they are added, because they may be
+ * in different queues: either the queue for potentially yielding tasks or the queue for yield-free tasks.
+ */
+ public static final YieldingAndFreeSimpleTaskQueue tickAssist = new YieldingAndFreeSimpleTaskQueue("TickAssist");
+
+ /**
+ * @see AllLevelsScheduledChunkCacheTaskQueue
+ */
+ public static final AllLevelsScheduledChunkCacheTaskQueue allLevelsScheduledChunkCache = new AllLevelsScheduledChunkCacheTaskQueue();
+
+ /**
+ * @see AllLevelsScheduledTickThreadChunkTaskQueue
+ */
+ public static final AllLevelsScheduledTickThreadChunkTaskQueue allLevelsScheduledTickThreadChunk = new AllLevelsScheduledTickThreadChunkTaskQueue();
+
+ /**
+ * This queue stores the tasks posted to {@link MCUtil#cleanerExecutor}.
+ */
+ public static final FreeSimpleTaskQueue cleaner = new FreeSimpleTaskQueue("Cleaner");
+
+ /**
+ * This queue stores the tasks scheduled to be executed on any thread, which would usually be stored in various
+ * executors with a specific purpose.
+ * <br>
+ * This queue may contain potentially yielding and yield-free tasks.
+ */
+ public static final YieldingAndFreeSimpleTaskQueue scheduledAsync = new YieldingAndFreeSimpleTaskQueue("ScheduledAsync");
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/queue/FreeSimpleTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/FreeSimpleTaskQueue.java
new file mode 100644
index 0000000000000000000000000000000000000000..7bffa7b358c058edbe33bbd2f31438c804c786ef
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/queue/FreeSimpleTaskQueue.java
@@ -0,0 +1,53 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.queue;
+
+import org.galemc.gale.concurrent.UnterminableExecutorService;
+import org.galemc.gale.executor.annotation.YieldFree;
+import org.galemc.gale.executor.annotation.thread.AnyThreadSafe;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * A base class for a task queue that contains tasks that are all yield-free.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+@YieldFree
+public class FreeSimpleTaskQueue extends SimpleTaskQueue {
+
+ FreeSimpleTaskQueue(String name) {
+ super(name, false, true);
+ }
+
+ FreeSimpleTaskQueue(String name, boolean lifoQueues) {
+ super(name, false, true, lifoQueues);
+ }
+
+ /**
+ * Schedules a new task to this queue.
+ *
+ * @param task The task to schedule.
+ */
+ @AnyThreadSafe
+ @YieldFree
+ public void add(Runnable task) {
+ this.add(task, false);
+ }
+
+ /**
+ * An executor for adding tasks to this queue,
+ * where {@link Executor#execute} calls {@link #add}.
+ */
+ public final ExecutorService executor = new UnterminableExecutorService() {
+
+ @Override
+ public void execute(@NotNull Runnable runnable) {
+ add(runnable, false);
+ }
+
+ };
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/queue/ScheduledServerThreadTaskQueues.java b/src/main/java/org/galemc/gale/executor/queue/ScheduledServerThreadTaskQueues.java
new file mode 100644
index 0000000000000000000000000000000000000000..12f676a679f4331b39e2c17273f0759a9ce1f5f0
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/queue/ScheduledServerThreadTaskQueues.java
@@ -0,0 +1,298 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.queue;
+
+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.util.thread.BlockableEventLoop;
+import org.galemc.gale.concurrent.UnterminableExecutorService;
+import org.galemc.gale.executor.annotation.Access;
+import org.galemc.gale.executor.annotation.thread.AnyThreadSafe;
+import org.galemc.gale.executor.annotation.Guarded;
+import org.galemc.gale.executor.annotation.YieldFree;
+import org.galemc.gale.executor.thread.pooled.ServerThread;
+import org.galemc.gale.executor.thread.wait.SignalReason;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Queue;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+/**
+ * This class stores the tasks scheduled to be executed on the main thread, which would usually be stored in a queue
+ * in the {@link MinecraftServer} in its role as a {@link BlockableEventLoop}. These tasks are not steps of
+ * ticking, but other tasks that must be executed at some point while no other main-thread-only task is running.
+ * <br>
+ * 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.
+ * <br>
+ * 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.
+ * <br>
+ * All contained tasks are currently assumed to be potentially yielding: no special distinction for yield-free
+ * tasks is made.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+@AnyThreadSafe
+@YieldFree
+public final class ScheduledServerThreadTaskQueues {
+
+ ScheduledServerThreadTaskQueues() {}
+
+ public static int DEFAULT_TASK_MAX_DELAY = 2;
+ public static int COMPLETE_CHUNK_FUTURE_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY;
+ public static int POST_CHUNK_LOAD_JOIN_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY;
+ public static int ANTI_XRAY_MODIFY_BLOCKS_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY;
+ public static int TELEPORT_ASYNC_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY;
+ public static int SEND_COMMAND_COMPLETION_SUGGESTIONS_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY;
+ public static int KICK_FOR_COMMAND_PACKET_SPAM_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY;
+ public static int KICK_FOR_RECIPE_PACKET_SPAM_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY;
+ public static int KICK_FOR_BOOK_TOO_LARGE_PACKET_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY;
+ public static int KICK_FOR_EDITING_BOOK_TOO_QUICKLY_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY;
+ public static int KICK_FOR_ILLEGAL_CHARACTERS_IN_CHAT_PACKET_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY;
+ public static int KICK_FOR_OUT_OF_ORDER_CHAT_PACKET_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY;
+ public static int HANDLE_DISCONNECT_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY;
+
+ /**
+ * A number of queues, with the queue at index i being the queue to be used after another i ticks pass
+ * even when {@link MinecraftServer#haveTime()} is false.
+ */
+ @SuppressWarnings({"rawtypes", "GrazieInspection"})
+ @Guarded(value = "#lock", except = "when optimistically reading using versionStamp as a stamp")
+ public static Queue[] queues = { new MultiThreadedQueue<>() };
+
+ /**
+ * <i>Probably</i> the lowest index of any queue in {@link #queues} that is non-empty.
+ * This is maintained as well as possible.
+ */
+ public static volatile int firstQueueWithPotentialTasksIndex = 0;
+
+ @Guarded(value = "#lock", fieldAccess = Access.WRITE)
+ public static volatile long versionStamp = 0;
+
+ private static final ReadWriteLock lock = new ReentrantReadWriteLock();
+ private static final Lock readLock = lock.readLock();
+ public static final Lock writeLock = lock.writeLock();
+
+ static SignalReason signalReason;
+
+ /**
+ * @return Whether there are any scheduled main thread tasks that could start right now. Tasks that are scheduled
+ * for a later tick will only be regarded as able to be started if both we are not out of spare time this tick
+ * and {@code tryNonCurrentTickQueuesAtAll} is true.
+ * <br>
+ * This method does not check whether the given thread is or could claim the main thread: whether a task
+ * can start now is thread-agnostic and based purely on the state of the queue.
+ */
+ public static boolean hasTasksThatCanStartNow(boolean tryNonCurrentTickQueuesAtAll) {
+ return hasTasks(tryNonCurrentTickQueuesAtAll, true);
+ }
+
+ /**
+ * @return Whether there are any scheduled main thread tasks, not counting any tasks that do not have to be
+ * finished within the current tick if {@code tryNonCurrentTickQueuesAtAll} is false.
+ */
+ public static boolean hasTasks(boolean tryNonCurrentTickQueuesAtAll) {
+ return hasTasks(tryNonCurrentTickQueuesAtAll, false);
+ }
+
+ /**
+ * Common implementation for {@link #hasTasksThatCanStartNow} and {@link #hasTasks(boolean)}.
+ */
+ private static boolean hasTasks(boolean tryNonCurrentTickQueuesAtAll, boolean checkCanStartNow) {
+ //noinspection StatementWithEmptyBody
+ while (!readLock.tryLock());
+ try {
+ // Try the queue most likely to contain tasks first
+ if (firstQueueWithPotentialTasksIndex == 0 || (tryNonCurrentTickQueuesAtAll && (!checkCanStartNow || !MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking))) {
+ if (!queues[firstQueueWithPotentialTasksIndex].isEmpty()) {
+ return true;
+ }
+ }
+ int checkUpTo = tryNonCurrentTickQueuesAtAll && (!checkCanStartNow || !MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking) ? queues.length : 1;
+ for (int i = 0; i < checkUpTo; i++) {
+ if (!queues[i].isEmpty()) {
+ return true;
+ }
+ }
+ } finally {
+ readLock.unlock();
+ }
+ return false;
+ }
+
+ /**
+ * Polls a task from this queue and returns it.
+ * Tasks that are scheduled for a later tick will only be regarded as able to be started if both we are not out of
+ * spare time this tick and {@code tryNonCurrentTickQueuesAtAll} is true.
+ * <br>
+ * This method does not check whether the given thread is or could claim the main thread: whether a task
+ * can start now is thread-agnostic and based purely on the state of the queue.
+ *
+ * @return The task that was polled, or null if no task was found.
+ */
+ public static @Nullable Runnable poll(ServerThread currentThread, boolean tryNonCurrentTickQueuesAtAll) {
+ // Since we assume the tasks in this queue to be potentially yielding, fail if the thread is restricted
+ if (currentThread.isRestrictedDueToYieldDepth) {
+ return null;
+ }
+ pollFromFirstQueueOrOthers: while (true) {
+ // Try to get a task from the first queue first
+ Object task = queues[0].poll();
+ if (task != null) {
+ return (Runnable) task;
+ } else if (!tryNonCurrentTickQueuesAtAll || MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking) {
+ // We needed a task from the first queue
+ if (queues[0].isEmpty()) {
+ // The first queue really is empty, so we fail
+ return null;
+ }
+ // An element was added to the first queue in the meantime, try again
+ continue;
+ }
+ tryGetRunnableUntilSuccessOrNothingChanged: while (true) {
+ boolean goOverAllQueues = firstQueueWithPotentialTasksIndex == 0;
+ long oldVersionStamp = versionStamp;
+ for (int i = goOverAllQueues ? 0 : firstQueueWithPotentialTasksIndex; i < queues.length; i++) {
+ if (!queues[i].isEmpty()) {
+ if (i == 0 || !MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking) {
+ task = queues[i].poll();
+ if (task == null) {
+ // We lost a race condition between the isEmpty() and poll() calls: just try again
+ continue tryGetRunnableUntilSuccessOrNothingChanged;
+ }
+ return (Runnable) task;
+ }
+ // Apparently we must now poll from the first queue only, try again
+ continue pollFromFirstQueueOrOthers;
+ }
+ // The queue was empty, the first queue with potential tasks must be the next one
+ if (i == firstQueueWithPotentialTasksIndex) {
+ if (i == queues.length - 1) {
+ firstQueueWithPotentialTasksIndex = 0;
+ } else {
+ firstQueueWithPotentialTasksIndex = i + 1;
+ }
+ }
+ }
+ if (goOverAllQueues && firstQueueWithPotentialTasksIndex == 0 && oldVersionStamp == versionStamp) {
+ /*
+ We went over all queues and nothing changed in the meantime,
+ we give up, there appear to be no more tasks.
+ */
+ return null;
+ }
+ // Something changed, or we did not go over all queues, try again
+ }
+ }
+ }
+
+ public static int getTaskCount() {
+ //noinspection StatementWithEmptyBody
+ while (!readLock.tryLock());
+ try {
+ int count = 0;
+ for (Queue<?> queue : queues) {
+ count += queue.size();
+ }
+ return count;
+ } finally {
+ readLock.unlock();
+ }
+ }
+
+ /**
+ * Schedules a new task to this queue.
+ *
+ * @param task The task to schedule.
+ *
+ * @deprecated Use {@link #add(Runnable, int)} instead: do not rely on {@link #DEFAULT_TASK_MAX_DELAY}.
+ */
+ @Deprecated
+ public static void add(Runnable task) {
+ add(task, DEFAULT_TASK_MAX_DELAY);
+ }
+
+ /**
+ * Schedules a new task to this queue.
+ *
+ * @param task The task to schedule.
+ * @param maxDelay The maximum number of ticks that the task must be finished by.
+ * A value of 0 means the task must be finished before the end of the current tick.
+ */
+ public static void add(Runnable task, int maxDelay) {
+ // Paper start - anything that does try to post to main during watchdog crash, run on watchdog
+ if (MinecraftServer.SERVER.hasStopped && Thread.currentThread() == MinecraftServer.SERVER.shutdownThread) {
+ task.run();
+ return;
+ }
+ // Paper end - anything that does try to post to main during watchdog crash, run on watchdog
+ //noinspection StatementWithEmptyBody
+ while (!readLock.tryLock());
+ try {
+ //noinspection NonAtomicOperationOnVolatileField
+ versionStamp++;
+ //noinspection unchecked
+ queues[maxDelay].add(task);
+ if (maxDelay < firstQueueWithPotentialTasksIndex) {
+ firstQueueWithPotentialTasksIndex = maxDelay;
+ }
+ //noinspection NonAtomicOperationOnVolatileField
+ versionStamp++;
+ } finally {
+ readLock.unlock();
+ }
+ MinecraftServer.nextTimeAssumeWeMayHaveDelayedTasks = true;
+ signalReason.signalAnother();
+ }
+
+ /**
+ * This moves the queues in {@link #queues}.
+ */
+ public static void shiftTasksForNextTick() {
+ //noinspection StatementWithEmptyBody
+ while (!writeLock.tryLock());
+ try {
+ //noinspection NonAtomicOperationOnVolatileField
+ versionStamp++;
+ // Move the queues to the preceding position
+ Queue<?> firstQueue = queues[0];
+ for (int i = 1; i < queues.length; i++) {
+ queues[i - 1] = queues[i];
+ }
+ // Re-use the same instance that was the old first queue as the new last queue
+ queues[queues.length - 1] = firstQueue;
+ // Move any elements that were still present in the previous first queue to the new first queue
+ //noinspection unchecked
+ queues[0].addAll(firstQueue);
+ firstQueue.clear();
+ //noinspection NonAtomicOperationOnVolatileField
+ versionStamp++;
+ } finally {
+ writeLock.unlock();
+ }
+ }
+
+ /**
+ * An executor for adding tasks to this queue,
+ * where {@link Executor#execute} calls {@link #add}.
+ *
+ * @deprecated Use {@link #add(Runnable, int)} instead: do not rely on {@link #DEFAULT_TASK_MAX_DELAY}.
+ */
+ @Deprecated
+ public static final ExecutorService executor = new UnterminableExecutorService() {
+
+ @Override
+ public void execute(@NotNull Runnable runnable) {
+ add(runnable);
+ }
+
+ };
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/queue/SimpleTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/SimpleTaskQueue.java
new file mode 100644
index 0000000000000000000000000000000000000000..7b0de8f921eb8431fa00f1e6a82769165a0efc00
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/queue/SimpleTaskQueue.java
@@ -0,0 +1,173 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.queue;
+
+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
+import org.galemc.gale.collection.FIFOConcurrentLinkedQueue;
+import org.galemc.gale.executor.annotation.thread.AnyThreadSafe;
+import org.galemc.gale.executor.annotation.YieldFree;
+import org.galemc.gale.executor.thread.BaseYieldingThread;
+import org.galemc.gale.executor.thread.pooled.AbstractYieldingSignallableThreadPool;
+import org.galemc.gale.executor.thread.wait.SignalReason;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Queue;
+
+/**
+ * A base class for a task queue that may contain tasks that are
+ * potentially yielding and tasks that are yield-free.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+@AnyThreadSafe
+@YieldFree
+public abstract class SimpleTaskQueue implements AbstractTaskQueue {
+
+ /**
+ * The name of this queue.
+ *
+ * @see #getName()
+ */
+ public final String name;
+
+ /**
+ * Whether tasks in this queue can be potentially yielding.
+ */
+ public final boolean canHaveYieldingTasks;
+
+ /**
+ * Whether tasks in this queue can be yield-free.
+ */
+ public final boolean canHaveFreeTasks;
+
+ /**
+ * The queue of potentially yielding tasks, or null if {@link #canHaveYieldingTasks} is false.
+ */
+ private final @Nullable Queue<Runnable> yieldingQueue;
+
+ /**
+ * The queue of yield-free tasks, or null if {@link #canHaveFreeTasks} is false.
+ */
+ private final @Nullable Queue<Runnable> freeQueue;
+
+ /**
+ * The {@link AbstractYieldingSignallableThreadPool} of which the member threads will poll from this queue.
+ * Will be initialized in {@link #setThreadPool} by the constructor of the thread pool.
+ */
+ public AbstractYieldingSignallableThreadPool threadPool;
+
+ /**
+ * The {@link SignalReason} used when signalling threads due to newly added potentially yielding tasks,
+ * or null if {@link #canHaveYieldingTasks} is false.
+ * Will be initialized in {@link #setThreadPool}.
+ */
+ private @Nullable SignalReason yieldingSignalReason;
+
+ /**
+ * The {@link SignalReason} used when signalling threads due to newly added yield-free tasks,
+ * or null if {@link #canHaveFreeTasks} is false.
+ * Will be initialized in {@link #setThreadPool}.
+ */
+ private @Nullable SignalReason freeSignalReason;
+
+ SimpleTaskQueue(String name, boolean canHaveYieldingTasks, boolean canHaveFreeTasks) {
+ this(name, canHaveYieldingTasks, canHaveFreeTasks, false);
+ }
+
+ /**
+ * @param name Value for {@link #getName}.
+ * @param canHaveYieldingTasks Value for {@link #canHaveYieldingTasks}.
+ * @param canHaveFreeTasks Value for {@link #canHaveFreeTasks}.
+ * @param lifoQueues If true, the {@link #yieldingQueue} and {@link #freeQueue} will be LIFO;
+ * otherwise, they will be FIFO.
+ */
+ SimpleTaskQueue(String name, boolean canHaveYieldingTasks, boolean canHaveFreeTasks, boolean lifoQueues) {
+ this.name = name;
+ this.canHaveYieldingTasks = canHaveYieldingTasks;
+ this.canHaveFreeTasks = canHaveFreeTasks;
+ this.yieldingQueue = this.canHaveYieldingTasks ? (lifoQueues ? new FIFOConcurrentLinkedQueue<>() : new MultiThreadedQueue<>()) : null;
+ this.freeQueue = this.canHaveFreeTasks ? (lifoQueues ? new FIFOConcurrentLinkedQueue<>() : new MultiThreadedQueue<>()) : null;
+ }
+
+ @Override
+ public String getName() {
+ return this.name;
+ }
+
+ @Override
+ public boolean hasYieldingTasksThatCanStartNow() {
+ //noinspection ConstantConditions
+ return this.canHaveYieldingTasks && !this.yieldingQueue.isEmpty();
+ }
+
+ @Override
+ public boolean hasFreeTasksThatCanStartNow() {
+ //noinspection ConstantConditions
+ return this.canHaveFreeTasks && !this.freeQueue.isEmpty();
+ }
+
+ @Override
+ public boolean hasTasks() {
+ //noinspection ConstantConditions
+ return (this.canHaveYieldingTasks && !this.yieldingQueue.isEmpty()) || (this.canHaveFreeTasks && !this.freeQueue.isEmpty());
+ }
+
+ @Override
+ public @Nullable Runnable poll(BaseYieldingThread currentThread) {
+ Runnable task;
+ if (!currentThread.isRestrictedDueToYieldDepth && this.canHaveYieldingTasks) {
+ //noinspection ConstantConditions
+ task = this.yieldingQueue.poll();
+ if (task != null) {
+ /*
+ If the thread was woken up for a different reason, signal for the same reason to wake up another
+ potential thread waiting for the reason.
+ */
+ SignalReason lastSignalReason = currentThread.lastSignalReason;
+ if (lastSignalReason != null && lastSignalReason != this.yieldingSignalReason) {
+ lastSignalReason.signalAnother();
+ }
+ return task;
+ }
+ }
+ if (this.canHaveFreeTasks) {
+ //noinspection ConstantConditions
+ task = this.freeQueue.poll();
+ if (task != null) {
+ /*
+ If the thread was woken up for a different reason, signal for the same reason to wake up another
+ potential thread waiting for the reason.
+ */
+ SignalReason lastSignalReason = currentThread.lastSignalReason;
+ if (lastSignalReason != null && lastSignalReason != this.freeSignalReason) {
+ lastSignalReason.signalAnother();
+ }
+ return task;
+ }
+ }
+ return null;
+ }
+
+ @SuppressWarnings("ConstantConditions")
+ @Override
+ public void add(Runnable task, boolean yielding) {
+ if (yielding) {
+ this.yieldingQueue.add(task);
+ this.yieldingSignalReason.signalAnother();
+ return;
+ }
+ this.freeQueue.add(task);
+ this.freeSignalReason.signalAnother();
+ }
+
+ @Override
+ public void setThreadPool(AbstractYieldingSignallableThreadPool threadPool) {
+ if (this.threadPool != null) {
+ throw new IllegalStateException("SimpleTaskQueue.threadPool was already initialized");
+ }
+ this.threadPool = threadPool;
+ this.yieldingSignalReason = SignalReason.createForThreadPoolNewTasks(this.threadPool, true);
+ this.freeSignalReason = SignalReason.createForThreadPoolNewTasks(this.threadPool, false);
+ }
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/queue/ThisTickScheduledServerThreadTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/ThisTickScheduledServerThreadTaskQueue.java
new file mode 100644
index 0000000000000000000000000000000000000000..a6bb58410d0bb741b44cf40a43269c848fdc5b51
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/queue/ThisTickScheduledServerThreadTaskQueue.java
@@ -0,0 +1,29 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.queue;
+
+import org.galemc.gale.executor.annotation.thread.AnyThreadSafe;
+import org.galemc.gale.executor.annotation.YieldFree;
+
+/**
+ * This class provides access to, but does not store, the tasks scheduled to be executed on the main thread,
+ * that must be finished in the current tick or its spare time.
+ * <br>
+ * This queue does not support {@link #add}.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+@AnyThreadSafe
+@YieldFree
+public final class ThisTickScheduledServerThreadTaskQueue extends TickRequiredScheduledServerThreadTaskQueue {
+
+ ThisTickScheduledServerThreadTaskQueue() {
+ super(false);
+ }
+
+ @Override
+ public String getName() {
+ return "ThisTickScheduledServerThread";
+ }
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/queue/TickRequiredScheduledServerThreadTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/TickRequiredScheduledServerThreadTaskQueue.java
new file mode 100644
index 0000000000000000000000000000000000000000..421ed98e13bfab6348e5f054e6aedeba89180de6
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/queue/TickRequiredScheduledServerThreadTaskQueue.java
@@ -0,0 +1,61 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.queue;
+
+import org.galemc.gale.executor.annotation.YieldFree;
+import org.galemc.gale.executor.annotation.thread.AnyThreadSafe;
+import org.galemc.gale.executor.thread.BaseYieldingThread;
+import org.galemc.gale.executor.thread.pooled.AbstractYieldingSignallableThreadPool;
+import org.galemc.gale.executor.thread.pooled.ServerThread;
+import org.galemc.gale.executor.thread.wait.SignalReason;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * A common base class for {@link ThisTickScheduledServerThreadTaskQueue} and
+ * {@link AnyTickScheduledServerThreadTaskQueue}.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+@AnyThreadSafe
+@YieldFree
+public abstract class TickRequiredScheduledServerThreadTaskQueue implements AbstractTaskQueue {
+
+ private final boolean tryNonCurrentTickQueuesAtAll;
+
+ protected TickRequiredScheduledServerThreadTaskQueue(boolean tryNonCurrentTickQueuesAtAll) {
+ this.tryNonCurrentTickQueuesAtAll = tryNonCurrentTickQueuesAtAll;
+ }
+
+ @Override
+ public boolean hasYieldingTasksThatCanStartNow() {
+ return ScheduledServerThreadTaskQueues.hasTasksThatCanStartNow(this.tryNonCurrentTickQueuesAtAll);
+ }
+
+ @Override
+ public boolean hasTasks() {
+ return ScheduledServerThreadTaskQueues.hasTasks(this.tryNonCurrentTickQueuesAtAll);
+ }
+
+ @Override
+ public boolean hasFreeTasksThatCanStartNow() {
+ return false;
+ }
+
+ @Override
+ public @Nullable Runnable poll(BaseYieldingThread currentThread) {
+ return ScheduledServerThreadTaskQueues.poll((ServerThread) currentThread, this.tryNonCurrentTickQueuesAtAll);
+ }
+
+ @Override
+ public void add(Runnable task, boolean yielding) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setThreadPool(AbstractYieldingSignallableThreadPool threadPool) {
+ if (ScheduledServerThreadTaskQueues.signalReason == null) {
+ ScheduledServerThreadTaskQueues.signalReason = SignalReason.createForThreadPoolNewTasks(threadPool, true);
+ }
+ }
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/queue/YieldingAndFreeSimpleTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/YieldingAndFreeSimpleTaskQueue.java
new file mode 100644
index 0000000000000000000000000000000000000000..95f8f83abd1a252cd1dbf805f7def97bb2455f9e
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/queue/YieldingAndFreeSimpleTaskQueue.java
@@ -0,0 +1,55 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.queue;
+
+import org.galemc.gale.concurrent.UnterminableExecutorService;
+import org.galemc.gale.executor.annotation.YieldFree;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * A base class for a task queue that contains tasks that are potentially yielding and tasks that are
+ * yield-free.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+@YieldFree
+public class YieldingAndFreeSimpleTaskQueue extends SimpleTaskQueue {
+
+ YieldingAndFreeSimpleTaskQueue(String name) {
+ super(name, true, true);
+ }
+
+ YieldingAndFreeSimpleTaskQueue(String name, boolean lifoQueues) {
+ super(name, true, true, lifoQueues);
+ }
+
+ /**
+ * An executor for adding potentially yielding tasks to this queue,
+ * where {@link Executor#execute} calls {@link #add}.
+ */
+ public final ExecutorService yieldingExecutor = new UnterminableExecutorService() {
+
+ @Override
+ public void execute(@NotNull Runnable runnable) {
+ add(runnable, true);
+ }
+
+ };
+
+ /**
+ * An executor for adding yield-free tasks to this queue,
+ * where {@link Executor#execute} calls {@link #add}.
+ */
+ public final ExecutorService freeExecutor = new UnterminableExecutorService() {
+
+ @Override
+ public void execute(@NotNull Runnable runnable) {
+ add(runnable, false);
+ }
+
+ };
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/queue/YieldingSimpleTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/YieldingSimpleTaskQueue.java
new file mode 100644
index 0000000000000000000000000000000000000000..015652cb18405fe44b4f8a5fe4b37549998ca795
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/queue/YieldingSimpleTaskQueue.java
@@ -0,0 +1,53 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.queue;
+
+import org.galemc.gale.concurrent.UnterminableExecutorService;
+import org.galemc.gale.executor.annotation.YieldFree;
+import org.galemc.gale.executor.annotation.thread.AnyThreadSafe;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * A base class for a task queue that contains tasks that are all potentially yielding.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+@YieldFree
+public class YieldingSimpleTaskQueue extends SimpleTaskQueue {
+
+ YieldingSimpleTaskQueue(String name) {
+ super(name, true, false);
+ }
+
+ YieldingSimpleTaskQueue(String name, boolean lifoQueues) {
+ super(name, true, false, lifoQueues);
+ }
+
+ /**
+ * Schedules a new task to this queue.
+ *
+ * @param task The task to schedule.
+ */
+ @AnyThreadSafe
+ @YieldFree
+ public void add(Runnable task) {
+ this.add(task, true);
+ }
+
+ /**
+ * An executor for adding tasks to this queue,
+ * where {@link Executor#execute} calls {@link #add}.
+ */
+ public final ExecutorService executor = new UnterminableExecutorService() {
+
+ @Override
+ public void execute(@NotNull Runnable runnable) {
+ add(runnable, true);
+ }
+
+ };
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/thread/AbstractYieldingThread.java b/src/main/java/org/galemc/gale/executor/thread/AbstractYieldingThread.java
new file mode 100644
index 0000000000000000000000000000000000000000..b7c51a6dc54f6879aa662c1431da1d684b348d2a
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/thread/AbstractYieldingThread.java
@@ -0,0 +1,69 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.thread;
+
+import org.galemc.gale.executor.annotation.thread.AnyThreadSafe;
+import org.galemc.gale.executor.annotation.PotentiallyYielding;
+import org.galemc.gale.executor.annotation.thread.ThisThreadOnly;
+import org.galemc.gale.executor.annotation.YieldFree;
+import org.galemc.gale.executor.lock.YieldingLock;
+import org.galemc.gale.executor.thread.pooled.TickAssistThread;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.function.BooleanSupplier;
+
+/**
+ * An interface for threads that can yield to other tasks in lieu of blocking.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+public interface AbstractYieldingThread extends SignallableThread {
+
+ /**
+ * Yields to tasks: polls and executes tasks while possible and the stop condition is not met.
+ * The stop condition is met if {@code stopCondition} is not null and returns true, or alternatively,
+ * if {@code stopCondition} is null, and {@code yieldingLock} is successfully acquired.
+ * When no tasks can be polled, this thread will block, waiting for either a task that can be executed by this
+ * thread to become available, or for the {@code yieldingLock}, if given, to be released.
+ * <br>
+ * Exactly one of {@code stopCondition} and {@code yieldingLock} must be non-null.
+ */
+ @ThisThreadOnly
+ @PotentiallyYielding("this method is meant to yield")
+ void yieldUntil(@Nullable BooleanSupplier stopCondition, @Nullable YieldingLock yieldingLock);
+
+ /**
+ * This method will keep attempting to find a task to do, and execute it, and if none is found, registering itself
+ * with the places where a relevant task may be added in order to be signalled when one is actually added.
+ * The loop is broken as soon as the stop condition becomes true, or the given lock is successfully acquired.
+ * <br>
+ * The above is the same as {@link #yieldUntil}, except it may be called in situations that is not 'yielding',
+ * for instance the endless loop polling tasks performed by a {@link TickAssistThread}. The difference with
+ * {@link #yieldUntil} is that this method does not increment or decrement things like the yield depth of this
+ * thread, if relevant.
+ *
+ * @see #yieldUntil
+ */
+ @ThisThreadOnly
+ @PotentiallyYielding("may yield further if an executed task is potentially yielding")
+ void runTasksUntil(@Nullable BooleanSupplier stopCondition, @Nullable YieldingLock yieldingLock);
+
+ /**
+ * @return The current thread if it is a {@link AbstractYieldingThread}, or null otherwise.
+ */
+ @AnyThreadSafe
+ @YieldFree
+ static @Nullable AbstractYieldingThread currentYieldingThread() {
+ return Thread.currentThread() instanceof AbstractYieldingThread yieldingThread ? yieldingThread : null;
+ }
+
+ /**
+ * @return Whether the current thread is a {@link AbstractYieldingThread}.
+ */
+ @AnyThreadSafe
+ @YieldFree
+ static boolean isYieldingThread() {
+ return Thread.currentThread() instanceof AbstractYieldingThread;
+ }
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/thread/BaseYieldingThread.java b/src/main/java/org/galemc/gale/executor/thread/BaseYieldingThread.java
new file mode 100644
index 0000000000000000000000000000000000000000..4502683211202b56bfc0988a74ba63ea8e45e15f
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/thread/BaseYieldingThread.java
@@ -0,0 +1,680 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.thread;
+
+import io.papermc.paper.util.TickThread;
+import net.minecraft.server.MinecraftServer;
+import org.galemc.gale.executor.annotation.Access;
+import org.galemc.gale.executor.annotation.thread.AnyThreadSafe;
+import org.galemc.gale.executor.annotation.Guarded;
+import org.galemc.gale.executor.annotation.PotentiallyBlocking;
+import org.galemc.gale.executor.annotation.thread.BaseYieldingThreadOnly;
+import org.galemc.gale.executor.annotation.thread.ThisThreadOnly;
+import org.galemc.gale.executor.annotation.YieldFree;
+import org.galemc.gale.executor.lock.YieldingLock;
+import org.galemc.gale.executor.queue.AbstractTaskQueue;
+import org.galemc.gale.executor.thread.pooled.ConditionalSurplusThread;
+import org.galemc.gale.executor.thread.wait.SignalReason;
+import org.galemc.gale.executor.thread.wait.WaitingThreadSet;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.BooleanSupplier;
+import java.util.stream.Collectors;
+
+/**
+ * An abstract base class implementing {@link AbstractYieldingThread},
+ * that provides implementation that is common between
+ * {@link TickThread} and {@link ConditionalSurplusThread}.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+public abstract class BaseYieldingThread extends Thread implements AbstractYieldingThread {
+
+ /**
+ * The minimum time to wait as the {@link MinecraftServer#serverThread} when performing a timed wait.
+ * Given in nanoseconds.
+ * If a timed wait with a lower time is attempted, the wait is not performed at all.
+ */
+ public static final long SERVER_THREAD_WAIT_NANOS_MINIMUM = 10_000;
+
+ /**
+ * The time to wait as the {@link MinecraftServer#serverThread} during the oversleep phase, if
+ * there may be delayed tasks.
+ * Given in nanoseconds.
+ */
+ public static final long SERVER_THREAD_WAIT_NANOS_DURING_OVERSLEEP_WITH_DELAYED_TASKS = 50_000;
+
+ /**
+ * The {@link SignalReason} to pass to {@link #signal(SignalReason)} when this thread becomes allowed
+ * to poll tasks after having not been allowed to.
+ */
+ public static final SignalReason becomesAllowedToPollSignalReason = SignalReason.createNonRetrying();
+
+ /**
+ * The queues that this thread can poll from, in the order they should be attempted to be polled from.
+ */
+ public final AbstractTaskQueue[] taskQueues;
+
+ /**
+ * The {@link WaitingThreadSet} to add this thread to while waiting for new potentially yielding tasks.
+ */
+ private final WaitingThreadSet threadsWaitingForYieldingTasks;
+
+ /**
+ * The {@link WaitingThreadSet} to add this thread to while waiting for new yield-free tasks.
+ */
+ private final WaitingThreadSet threadsWaitingForFreeTasks;
+
+ /**
+ * Whether this thread keeps track of the yield depth in {@link #yieldDepth}.
+ */
+ private final boolean trackYieldDepth;
+
+ /**
+ * The maximum yield depth. While this thread has a yield depth equal to or greater than this value,
+ * it can not start more potentially yielding tasks.
+ * <br>
+ * This value has no effect if {@link #trackYieldDepth} is false.
+ */
+ public final int maximumYieldDepth;
+
+ /**
+ * The current yield depth of this thread.
+ * <br>
+ * This value is always 0 if {@link #trackYieldDepth} is false.
+ */
+ @AnyThreadSafe(Access.READ) @ThisThreadOnly(Access.WRITE)
+ public volatile int yieldDepth = 0;
+
+ /**
+ * Whether this thread is currently restricted
+ * due to {@link #yieldDepth} being at least {@link #maximumYieldDepth}.
+ * <br>
+ * This value is always false if {@link #trackYieldDepth} is false.
+ */
+ @AnyThreadSafe(Access.READ) @ThisThreadOnly(Access.WRITE)
+ public volatile boolean isRestrictedDueToYieldDepth = false;
+
+ /**
+ * The lock to guard this thread's sleeping and waking actions.
+ */
+ protected final Lock waitLock = new ReentrantLock();
+
+ /**
+ * The condition to wait for a signal, when this thread has to wait for something to do.
+ */
+ protected final Condition waitCondition = waitLock.newCondition();
+
+ /**
+ * Whether this thread is currently not working on the content of a task, but instead
+ * attempting to poll a next task to do, checking whether it can accept tasks at all, or
+ * attempting to acquire a {@link YieldingLock}.
+ * <br>
+ * This value is used to determine whether to set {@link #skipNextWait} when {@link #signal} is called.
+ */
+ @AnyThreadSafe(Access.READ) @ThisThreadOnly(Access.WRITE)
+ private volatile boolean isPollingTaskOrCheckingStopCondition = true;
+
+ /**
+ * Whether this thread should not start waiting for something to do the next time no task could be polled,
+ * but instead try polling a task again.
+ */
+ @AnyThreadSafe
+ private volatile boolean skipNextWait = false;
+
+ /**
+ * Whether this thread is currently waiting for something to do.
+ */
+ @AnyThreadSafe(Access.READ) @ThisThreadOnly(Access.WRITE)
+ @Guarded(value = "#waitLock", fieldAccess = Access.WRITE)
+ public volatile boolean isWaiting = false;
+
+ /**
+ * If {@link #isWaiting} is true, whether this thread is registered to be signalled when new tasks are added.
+ * This value is meaningless while {@link #isWaiting} is false.
+ */
+ public volatile boolean isRegisteredForNewTasks;
+
+ /**
+ * The last reason this thread was signalled before the current poll attempt, or null if the current
+ * poll attempt was not preceded by signalling (but by yielding for example).
+ */
+ public volatile @Nullable SignalReason lastSignalReason = null;
+
+ protected BaseYieldingThread(Runnable target, String name, AbstractTaskQueue[] taskQueues, WaitingThreadSet threadsWaitingForYieldingTasks, WaitingThreadSet threadsWaitingForFreeTasks, boolean trackYieldDepth, int maximumYieldDepth) {
+ super(target, name);
+ this.taskQueues = taskQueues;
+ this.threadsWaitingForYieldingTasks = threadsWaitingForYieldingTasks;
+ this.threadsWaitingForFreeTasks = threadsWaitingForFreeTasks;
+ this.trackYieldDepth = trackYieldDepth;
+ this.maximumYieldDepth = maximumYieldDepth;
+ }
+
+ /**
+ * To be called when this thread wants to poll a task.
+ *
+ * @return Whether the polling will be allowed to proceed. If false, the thread will start waiting
+ * until signalled by {@link #becomesAllowedToPollSignalReason} or another reason that is not a new task being
+ * added to a queue (for example a {@link YieldingLock} it is trying to acquire being released).
+ */
+ protected abstract boolean wantsToPoll();
+
+ /**
+ * To be called when this thread could not poll a task and is going to wait.
+ * This is not called when the thread goes to sleep due to receiving a {@code false} result from
+ * {@link #wantsToPoll}.
+ *
+ * @return Whether this thread should register as waiting for new tasks to be added.
+ */
+ protected abstract boolean willWaitAfterPollFailure();
+
+ /**
+ * To be called when this thread has been signalled by a reason other than
+ * {@link #becomesAllowedToPollSignalReason}, and waking up from {@link Condition#await}.
+ */
+ protected abstract void wokeUpAfterNonBecomeAllowedToPollSignal();
+
+ /**
+ * To be called right before this thread will begin to block.
+ */
+ protected abstract void preBlockThread();
+
+ /**
+ * To be called right after this thread blocked.
+ */
+ protected abstract void postBlockThread();
+
+ @Override
+ public void yieldUntil(@Nullable BooleanSupplier stopCondition, @Nullable YieldingLock yieldingLock) {
+ if (this.trackYieldDepth) {
+ //noinspection NonAtomicOperationOnVolatileField
+ this.yieldDepth++;
+ if (!this.isRestrictedDueToYieldDepth && this.yieldDepth >= this.maximumYieldDepth) {
+ this.isRestrictedDueToYieldDepth = true;
+ }
+ }
+ this.runTasksUntil(stopCondition, yieldingLock);
+ if (this.trackYieldDepth) {
+ //noinspection NonAtomicOperationOnVolatileField
+ this.yieldDepth--;
+ if (this.isRestrictedDueToYieldDepth && this.yieldDepth < this.maximumYieldDepth) {
+ this.isRestrictedDueToYieldDepth = false;
+ }
+ }
+ }
+
+ @Override
+ public void runTasksUntil(@Nullable BooleanSupplier stopCondition, @Nullable YieldingLock yieldingLock) {
+ this.isPollingTaskOrCheckingStopCondition = true;
+
+ /*
+ Endless loop that attempts to perform a task, and if one is found, tries to perform another again,
+ but if none is found, starts awaiting such a task to become available, or for the given yielding lock
+ to be released.
+ */
+ while (true) {
+ if (stopCondition != null) {
+ if (this == MinecraftServer.serverThread) {
+ MinecraftServer.currentManagedBlockStopConditionHasBecomeTrue = false;
+ }
+ if (stopCondition.getAsBoolean()) {
+ if (this == MinecraftServer.serverThread) {
+ MinecraftServer.currentManagedBlockStopConditionHasBecomeTrue = true;
+ }
+ break;
+ }
+ } else {
+ //noinspection ConstantConditions
+ if (yieldingLock.tryLock()) {
+ break;
+ }
+ }
+
+ // Check if this thread is allowed to poll
+ if (!wantsToPoll()) {
+ // If not, the thread will wait until it is allowed, and then try the loop again
+ this.waitUntilSignalled(yieldingLock, false, false);
+ continue;
+ }
+
+ // If this is the server thread, update isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking
+ if (this == MinecraftServer.serverThread) {
+ MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking = MinecraftServer.isInSpareTime && MinecraftServer.blockingCount == 0 && !MinecraftServer.SERVER.haveTime();
+ }
+
+ // Attempt to poll a task that can be started
+ Runnable task = this.pollTask();
+
+ // Run the task if found
+ if (task != null) {
+
+ // If this is the server thread, potentially set nextTimeAssumeWeMayHaveDelayedTasks to true
+ if (this == MinecraftServer.serverThread && !MinecraftServer.nextTimeAssumeWeMayHaveDelayedTasks && AbstractTaskQueue.taskQueuesHaveTasks(this.taskQueues)) {
+ MinecraftServer.nextTimeAssumeWeMayHaveDelayedTasks = true;
+ }
+
+ this.isPollingTaskOrCheckingStopCondition = false;
+ task.run();
+
+ // If this is the server thread, execute some chunk tasks
+ if (this == MinecraftServer.serverThread) {
+ MinecraftServer.SERVER.executeMidTickTasks(); // Paper - execute chunk tasks mid tick
+ }
+
+ this.isPollingTaskOrCheckingStopCondition = true;
+ continue;
+
+ }
+
+ /*
+ If no task that can be started was found, wait for one to become available,
+ or for the given yielding lock to be released. This is the only time we should ever block inside
+ a potentially yielding procedure.
+ */
+ this.waitUntilSignalled(yieldingLock, true, true);
+
+ }
+
+ this.isPollingTaskOrCheckingStopCondition = false;
+
+ /*
+ If the thread was signalled for another reason than the lock, but we acquired the lock instead,
+ another thread should be signalled.
+ */
+ SignalReason lastSignalReason = this.lastSignalReason;
+ if (lastSignalReason != null && yieldingLock != null && lastSignalReason != yieldingLock.getSignalReason()) {
+ lastSignalReason.signalAnother();
+ }
+
+ }
+
+ /**
+ * Polls a task from any queue this thread can currently poll from, and returns it.
+ * Polling potentially yielding tasks is attempted before yield-free tasks.
+ *
+ * @return The task that was polled, or null if no task was found.
+ */
+ @ThisThreadOnly
+ @YieldFree
+ protected @Nullable Runnable pollTask() {
+ for (var queue : this.taskQueues) {
+ Runnable task = queue.poll(this);
+ if (task != null) {
+ return task;
+ }
+ }
+ // We failed to poll any task
+ return null;
+ }
+
+ /**
+ * Starts waiting on something to do.
+ *
+ * @param yieldingLock A {@link YieldingLock} to register with, or null if this thread is not waiting for
+ * a yielding lock.
+ * @param registerForTasks Whether to register to wait for new tasks being added to the task queues.
+ * @param callWillWait Whether to call {@link #willWaitAfterPollFailure()}
+ */
+ @ThisThreadOnly
+ @PotentiallyBlocking
+ private void waitUntilSignalled(@Nullable YieldingLock yieldingLock, boolean registerForTasks, boolean callWillWait) {
+
+ if (callWillWait) {
+ if (!this.willWaitAfterPollFailure()) {
+ registerForTasks = false;
+ }
+ }
+
+ // Remember whether we registered to wait, to unregister later
+ boolean registeredAsWaiting = false;
+ this.isRegisteredForNewTasks = false;
+ // No point in registering if we're not going to wait anyway
+ if (!this.skipNextWait) {
+ // Register this thread with parties that may signal it
+ this.registerAsWaiting(yieldingLock, registerForTasks);
+ registeredAsWaiting = true;
+ }
+
+ /*
+ If we cannot acquire the lock, we can assume this thread is being signalled,
+ so there is no reason to start waiting.
+ */
+ waitWithLock: if (this.waitLock.tryLock()) {
+ try {
+
+ // If it was set that this thread should skip the wait in the meantime, skip it
+ if (this.skipNextWait) {
+ break waitWithLock;
+ }
+
+ // Do a quick last check to not wait if a new task that can be polled was added in the meantime
+ if (registerForTasks && AbstractTaskQueue.taskQueuesHaveTasksCouldStart(this.taskQueues, this)) {
+ break waitWithLock;
+ }
+
+ // Register as waiting
+ if (!registeredAsWaiting) {
+ this.registerAsWaiting(yieldingLock, registerForTasks);
+ registeredAsWaiting = true;
+ }
+
+ // Mark this thread as waiting
+ this.isWaiting = true;
+
+ // Wait
+ try {
+
+ /*
+ Check if we should wait with a timeout: this only happens if this thread is the server thread, in
+ which case we do not want to wait past the start of the next tick.
+ */
+ boolean waitedWithTimeout = false;
+ if (this == MinecraftServer.serverThread) {
+ // -1 indicates to not use a timeout (this value is not later set to any other negative value)
+ long waitForNanos = -1;
+ if (MinecraftServer.isWaitingUntilNextTick) {
+ /*
+ During waiting until the next tick, we wait until the next tick start.
+ If it already passed, we do not have to use a timeout, because we will be notified
+ when the stop condition becomes true.
+ */
+ waitForNanos = MinecraftServer.nextTickStartNanoTime - System.nanoTime();
+ if (waitForNanos < 0) {
+ waitForNanos = -1;
+ }
+ } else if (MinecraftServer.SERVER.isOversleep) {
+ /*
+ During this phase, MinecraftServer#mayHaveDelayedTasks() is checked, and we may not
+ be notified when it changes. Therefore, if the next tick start has not passed, we will
+ wait until then, but if it has, we wait for a short interval to make sure we keep
+ checking the stop condition (but not for longer than until the last time we can be
+ executing extra delayed tasks).
+ */
+ waitForNanos = MinecraftServer.nextTickStartNanoTime - System.nanoTime();
+ if (waitForNanos < 0) {
+ waitForNanos = Math.min(Math.max(0, MinecraftServer.delayedTasksMaxNextTickNanoTime - System.nanoTime()), SERVER_THREAD_WAIT_NANOS_DURING_OVERSLEEP_WITH_DELAYED_TASKS);
+ }
+ }
+ if (waitForNanos >= 0) {
+ // Set the last signal reason to null in case the timeout elapses without a signal
+ this.lastSignalReason = null;
+ // Wait, but at most for the determined time
+ waitedWithTimeout = true;
+ // Skip if the time is too short
+ if (waitForNanos >= SERVER_THREAD_WAIT_NANOS_MINIMUM) {
+ this.preBlockThread();
+ //noinspection ResultOfMethodCallIgnored
+ this.waitCondition.await(waitForNanos, TimeUnit.NANOSECONDS);
+ this.postBlockThread();
+ }
+ }
+ }
+
+ /*
+ If we did not wait with a timeout, wait indefinitely. If this thread is the server thread,
+ and the intended start time of the next tick has already passed, but the stop condition to stop
+ running tasks is still not true, this thread must be signalled when a change in conditions causes
+ the stop condition to become true.
+ */
+ if (!waitedWithTimeout) {
+ this.preBlockThread();
+ this.waitCondition.await();
+ this.postBlockThread();
+ }
+
+ } catch (InterruptedException e) {
+ throw new IllegalStateException(e);
+ }
+
+ // Unmark this thread as waiting
+ this.isWaiting = false;
+
+ } finally {
+ this.waitLock.unlock();
+ }
+ }
+
+ if (this.lastSignalReason != becomesAllowedToPollSignalReason) {
+ this.wokeUpAfterNonBecomeAllowedToPollSignal();
+ }
+
+ // Unregister this thread from the parties it was registered with before
+ if (registeredAsWaiting) {
+ this.unregisterAsWaiting(yieldingLock);
+ }
+
+ // Reset skipping the next wait
+ this.skipNextWait = false;
+
+ }
+
+ /**
+ * Registers this thread in various places that will help to identify this thread as a candidate to signal
+ * in case of changes that allow this thread to continue work.
+ *
+ * @param yieldingLock A {@link YieldingLock} to register with, or null if this thread is not waiting for
+ * a yielding lock.
+ * @param registerForTasks Whether to register to wait for new tasks being added to the task queues.
+ */
+ @ThisThreadOnly
+ @YieldFree
+ private void registerAsWaiting(@Nullable YieldingLock yieldingLock, boolean registerForTasks) {
+ // Register with the queues that we may poll tasks from
+ if (registerForTasks && !this.isRegisteredForNewTasks) {
+ this.registerAsWaitingForTasks(false);
+ }
+ // Register with the lock
+ if (yieldingLock != null) {
+ yieldingLock.addWaitingThread(this);
+ }
+ }
+
+ /**
+ * Unregisters this thread as waiting for new tasks be added.
+ * <br>
+ * This must either be called from this thread itself while {@link #isWaiting} is false,
+ * with {@code mustAcquireLock} given as false,
+ * or be called from any thread while {@link #isWaiting} is true,
+ * with {@code mustAcquireLock} given as true.
+ * <br>
+ * This must only be called when {@link #isRegisteredForNewTasks} is false.
+ *
+ * @see #registerAsWaiting
+ */
+ @YieldFree
+ public void registerAsWaitingForTasks(boolean mustAcquireLock) {
+ if (mustAcquireLock) {
+ //noinspection StatementWithEmptyBody
+ while (!this.waitLock.tryLock());
+ }
+ try {
+ this.threadsWaitingForYieldingTasks.add(this);
+ if (!this.isRestrictedDueToYieldDepth) {
+ this.threadsWaitingForFreeTasks.add(this);
+ }
+ this.isRegisteredForNewTasks = true;
+ } finally {
+ if (mustAcquireLock) {
+ this.waitLock.unlock();
+ }
+ }
+ }
+
+ /**
+ * Unregisters this thread from the places it was registered with in {@link #registerAsWaiting}.
+ *
+ * @see #registerAsWaiting
+ */
+ @ThisThreadOnly
+ @YieldFree
+ private void unregisterAsWaiting(@Nullable YieldingLock yieldingLock) {
+ // Unregister from the task queues
+ if (this.isRegisteredForNewTasks) {
+ this.unregisterAsWaitingForTasks(false);
+ }
+ // Unregister from the lock
+ if (yieldingLock != null) {
+ yieldingLock.removeWaitingThread(this);
+ }
+ }
+
+ /**
+ * Unregisters this thread as waiting for new tasks be added.
+ * <br>
+ * This must either be called from this thread itself while {@link #isWaiting} is false,
+ * with {@code mustAcquireLock} given as false,
+ * or be called from any thread while {@link #isWaiting} is true,
+ * with {@code mustAcquireLock} given as true.
+ * <br>
+ * This must only be called when {@link #isRegisteredForNewTasks} is true.
+ *
+ * @see #unregisterAsWaiting
+ */
+ @YieldFree
+ public void unregisterAsWaitingForTasks(boolean mustAcquireLock) {
+ if (mustAcquireLock) {
+ //noinspection StatementWithEmptyBody
+ while (!this.waitLock.tryLock());
+ }
+ try {
+ this.threadsWaitingForYieldingTasks.remove(this);
+ if (!this.isRestrictedDueToYieldDepth) {
+ this.threadsWaitingForFreeTasks.remove(this);
+ }
+ this.isRegisteredForNewTasks = false;
+ } finally {
+ if (mustAcquireLock) {
+ this.waitLock.unlock();
+ }
+ }
+ }
+
+ /**
+ * Signals this thread to wake up, or if it was not sleeping but attempting to poll a task:
+ * to not go to sleep the next time no task could be polled, and instead try polling a task again.
+ *
+ * @param reason The reason why this thread was signalled, or null if it is irrelevant (e.g. when the signal
+ * will never need to be repeated because there is only thread waiting for this specific event
+ * to happen).
+ * @return Whether this thread was sleeping before, and has woken up now,
+ * or whether {@link #skipNextWait} was set to true.
+ */
+ @AnyThreadSafe
+ @YieldFree
+ public boolean signal(@Nullable SignalReason reason) {
+ //noinspection StatementWithEmptyBody
+ while (!this.waitLock.tryLock());
+ try {
+ if (this.isWaiting) {
+ this.lastSignalReason = reason;
+ this.waitCondition.signal();
+ return true;
+ } else if (this.isPollingTaskOrCheckingStopCondition && !this.skipNextWait) {
+ this.lastSignalReason = reason;
+ this.skipNextWait = true;
+ return true;
+ }
+ return false;
+ } finally {
+ this.waitLock.unlock();
+ }
+ }
+
+ /**
+ * Signals this thread to wake up. Has no effect if {@link #isWaiting} is false.
+ *
+ * @param reason The reason why this thread was signalled, or null if it is irrelevant (e.g. when the signal
+ * will never need to be repeated because there is only thread waiting for this specific event
+ * to happen).
+ * @return Whether this thread was sleeping before, and has woken up now.
+ */
+ @AnyThreadSafe
+ @YieldFree
+ public boolean signalIfWaiting(@Nullable SignalReason reason) {
+ //noinspection StatementWithEmptyBody
+ while (!this.waitLock.tryLock());
+ try {
+ if (this.isWaiting) {
+ this.lastSignalReason = reason;
+ this.waitCondition.signal();
+ return true;
+ }
+ return false;
+ } finally {
+ this.waitLock.unlock();
+ }
+ }
+
+ /**
+ * Allows this thread to poll, either by registering it to listen for tasks if it was not registered,
+ * or by signalling it if tasks that can be started are already present.
+ *
+ * @return Whether this thread was not active before, and is active now.
+ */
+ @AnyThreadSafe
+ @YieldFree
+ public boolean activateIfNotListeningForTasksAndNotRestricted() {
+ //noinspection StatementWithEmptyBody
+ while (!this.waitLock.tryLock());
+ try {
+ if (this.isWaiting && !this.isRegisteredForNewTasks && !this.isRestrictedDueToYieldDepth) {
+ if (AbstractTaskQueue.taskQueuesHaveTasks(this.taskQueues)) {
+ this.lastSignalReason = becomesAllowedToPollSignalReason;
+ this.waitCondition.signal();
+ } else {
+ this.registerAsWaitingForTasks(false);
+ }
+ return true;
+ }
+ return false;
+ } finally {
+ this.waitLock.unlock();
+ }
+ }
+
+ /**
+ * Unregisters this thread to listen for tasks if it was not registered.
+ *
+ * @return Whether this thread was sleeping and registered to listen for tasks before,
+ * and was unregistered to listen for tasks.
+ */
+ @AnyThreadSafe
+ @YieldFree
+ public boolean deactivateIfListeningForTasks() {
+ //noinspection StatementWithEmptyBody
+ while (!this.waitLock.tryLock());
+ try {
+ if (this.isWaiting && this.isRegisteredForNewTasks) {
+ this.unregisterAsWaitingForTasks(false);
+ return true;
+ }
+ return false;
+ } finally {
+ this.waitLock.unlock();
+ }
+ }
+ /**
+ * Causes this thread to loop forever, always attempting to find a task to do, and if none is found,
+ * registering itself with the places where a relevant task may be added in order to be signalled when
+ * one is actually added.
+ */
+ @ThisThreadOnly
+ protected void runForever() {
+ this.runTasksUntil(() -> false, null);
+ }
+
+ /**
+ * A method that simply acquires the {@link BaseYieldingThread} that is the current thread, and calls
+ * {@link #runForever()} on it.
+ */
+ @BaseYieldingThreadOnly
+ protected static void getCurrentBaseYieldingThreadAndRunForever() {
+ ((BaseYieldingThread) Thread.currentThread()).runForever();
+ }
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/thread/OriginalServerThread.java b/src/main/java/org/galemc/gale/executor/thread/OriginalServerThread.java
new file mode 100644
index 0000000000000000000000000000000000000000..c44607129a79884b1e4f3d9fb8b57cdfa527d267
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/thread/OriginalServerThread.java
@@ -0,0 +1,21 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.thread;
+
+import net.minecraft.server.MinecraftServer;
+import org.galemc.gale.executor.thread.pooled.ServerThread;
+import org.spigotmc.WatchdogThread;
+
+/**
+ * A type that is unique to {@link MinecraftServer#serverThread},
+ * to distinguish it from {@link WatchdogThread#instance}.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+public final class OriginalServerThread extends ServerThread {
+
+ public OriginalServerThread(final Runnable run, final String name) {
+ super(run, name);
+ }
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/thread/SignallableThread.java b/src/main/java/org/galemc/gale/executor/thread/SignallableThread.java
new file mode 100644
index 0000000000000000000000000000000000000000..0d6cf71da1100c51e3b8aaf8e28765bc82e7eeb5
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/thread/SignallableThread.java
@@ -0,0 +1,32 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.thread;
+
+import org.galemc.gale.executor.annotation.thread.AnyThreadSafe;
+import org.galemc.gale.executor.annotation.YieldFree;
+import org.galemc.gale.executor.thread.wait.SignalReason;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * An interface for threads that can wait (either by blocking or yielding) for events, and be signalled when
+ * circumstances may have changed.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+public interface SignallableThread {
+
+ /**
+ * Signals this thread to wake up, or if it was not sleeping but attempting to poll a task:
+ * to not go to sleep the next time no task could be polled, and instead try polling a task again.
+ *
+ * @param reason The reason why this thread was signalled, or null if it is irrelevant (e.g. when the signal
+ * will never need to be repeated because there is only thread waiting for this specific event
+ * to happen).
+ * @return Whether this thread was sleeping before, and had not been signalled to wake up before,
+ * but has or will be woken up due to this signal.
+ */
+ @AnyThreadSafe
+ @YieldFree
+ boolean signal(@Nullable SignalReason reason);
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/thread/deferral/ServerThreadDeferral.java b/src/main/java/org/galemc/gale/executor/thread/deferral/ServerThreadDeferral.java
new file mode 100644
index 0000000000000000000000000000000000000000..9d02c2f4360bf19f379b7012206b72abffda886a
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/thread/deferral/ServerThreadDeferral.java
@@ -0,0 +1,136 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.thread.deferral;
+
+import io.papermc.paper.util.TickThread;
+import org.galemc.gale.concurrent.UnterminableExecutorService;
+import org.galemc.gale.executor.queue.BaseTaskQueues;
+import org.galemc.gale.executor.thread.AbstractYieldingThread;
+import org.galemc.gale.executor.thread.pooled.ServerThread;
+import org.galemc.gale.executor.thread.pooled.TickAssistThread;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.function.Supplier;
+
+/**
+ * This class provides functionality to allow any thread,
+ * including but not limited to a {@link TickAssistThread},
+ * to defer blocks of code to a {@link ServerThread}, and wait for its completion.
+ * <br>
+ * Using deferral from a {@link TickThread} that is not the correct thread already is highly discouraged
+ * because yielding from a {@link TickThread} should be avoided whenever possible.
+ *
+ * @see TickThreadDeferral
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+public final class ServerThreadDeferral {
+
+ private ServerThreadDeferral() {}
+
+ /**
+ * @see #defer(Supplier, boolean)
+ */
+ public static void defer(Runnable task, boolean yielding) {
+ deferInternal(task, null, yielding);
+ }
+
+ /**
+ * Defers the given {@code task} to a {@link ServerThread}, and yields until it has finished.
+ * If this thread is a {@link ServerThread}, the task will be executed right away.
+ * <br>
+ * 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.
+ * <br>
+ * 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.
+ * <br>
+ * If this thread is already an appropriate thread to run the task on, the task is performed on this thread.
+ *
+ * @param task The task to run.
+ * @param yielding Whether the task is potentially yielding.
+ */
+ public static <T> T defer(Supplier<T> task, boolean yielding) {
+ return deferInternal(null, task, yielding);
+ }
+
+ /**
+ * Common implementation for {@link #defer(Runnable, boolean)} and {@link #defer(Supplier, boolean)}.
+ * Exactly one of {@code runnable} or {@code supplier} must be non-null.
+ */
+ private static <T> T deferInternal(@Nullable Runnable runnable, @Nullable Supplier<T> supplier, boolean yielding) {
+ // Check if we are the right thread
+ if (TickThread.isTickThread()) {
+ if (runnable == null) {
+ //noinspection ConstantConditions
+ return supplier.get();
+ }
+ runnable.run();
+ return null;
+ }
+ // Otherwise, schedule the task and wait for it to complete
+ CompletableFuture<T> future = new CompletableFuture<>();
+ AbstractYieldingThread yieldingThread = AbstractYieldingThread.currentYieldingThread();
+ if (yieldingThread != null) {
+ // Yield until the task completes
+ BaseTaskQueues.deferredToServerThread.add(() -> {
+ if (runnable == null) {
+ //noinspection ConstantConditions
+ future.complete(supplier.get());
+ } else {
+ runnable.run();
+ future.complete(null);
+ }
+ yieldingThread.signal(null);
+ }, yielding);
+ yieldingThread.yieldUntil(future::isDone, null);
+ return future.getNow(null);
+ } else {
+ // Block until the task completes
+ BaseTaskQueues.deferredToServerThread.add(() -> {
+ if (runnable == null) {
+ //noinspection ConstantConditions
+ future.complete(supplier.get());
+ } else {
+ runnable.run();
+ future.complete(null);
+ }
+ }, yielding);
+ return future.join();
+ }
+ }
+
+ /**
+ * An executor for deferring potentially yielding, tasks to the main thread,
+ * where {@link Executor#execute} calls {@link #defer}.
+ */
+ public static final ExecutorService yieldingExecutor = new UnterminableExecutorService() {
+
+ @Override
+ public void execute(@NotNull Runnable runnable) {
+ defer(runnable, true);
+ }
+
+ };
+
+ /**
+ * An executor for deferring yield-free, tasks to the main thread,
+ * where {@link Executor#execute} calls {@link #defer}.
+ */
+ public static final ExecutorService freeExecutor = new UnterminableExecutorService() {
+
+ @Override
+ public void execute(@NotNull Runnable runnable) {
+ defer(runnable, false);
+ }
+
+ };
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/thread/deferral/TickThreadDeferral.java b/src/main/java/org/galemc/gale/executor/thread/deferral/TickThreadDeferral.java
new file mode 100644
index 0000000000000000000000000000000000000000..74319026715d9a31e2d34fcba251d6d0e828ffe5
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/thread/deferral/TickThreadDeferral.java
@@ -0,0 +1,143 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.thread.deferral;
+
+import io.papermc.paper.util.TickThread;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.entity.Entity;
+import org.galemc.gale.concurrent.UnterminableExecutorService;
+import org.galemc.gale.executor.thread.AbstractYieldingThread;
+import org.galemc.gale.executor.thread.pooled.TickAssistThread;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.function.Supplier;
+
+/**
+ * This class provides functionality to allow any thread,
+ * including but not limited to a {@link TickAssistThread},
+ * to defer blocks of code to a {@link TickThread}, and wait for its completion.
+ * In other words, instead of the typical paradigm where a block of code is executed
+ * while a lock is held by the thread, we do not acquire a lock, but instead schedule the code
+ * to be run on a thread responsible for the specific aspects of the code
+ * (thereby avoiding deadlocks caused by the acquisition of multiple locks in various orders,
+ * and avoiding collisions between parts of code that can not run concurrently,
+ * which occur especially easy in parts of code that may have to call callbacks of which
+ * we can only make limited assumptions) and wait for that to finish.
+ * <br>
+ * 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).
+ * <br>
+ * 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.
+ * <br>
+ * Using deferral from a {@link TickThread} that is not the correct thread already is highly discouraged
+ * because yielding from a {@link TickThread} should be avoided whenever possible.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+public final class TickThreadDeferral {
+
+ private TickThreadDeferral() {}
+
+ /**
+ * This may be useful in the future. See the documentation of {@link TickThread#taskQueues}.
+ *
+ * @see #defer(Runnable, boolean)
+ */
+ public static void defer(final ServerLevel world, final int chunkX, final int chunkZ, Runnable task, boolean yielding) {
+ defer(task, yielding);
+ }
+
+ /**
+ * This may be useful in the future. See the documentation of {@link TickThread#taskQueues}.
+ *
+ * @see #defer(Supplier, boolean)
+ */
+ public static <T> T defer(final ServerLevel world, final int chunkX, final int chunkZ, Supplier<T> task, boolean yielding) {
+ return defer(task, yielding);
+ }
+
+ /**
+ * This may be useful in the future. See the documentation of {@link TickThread#taskQueues}.
+ *
+ * @see #defer(Runnable, boolean)
+ */
+ public static void defer(final Entity entity, Runnable task, boolean yielding) {
+ defer(task, yielding);
+ }
+
+ /**
+ * This may be useful in the future. See the documentation of {@link TickThread#taskQueues}.
+ *
+ * @see #defer(Supplier, boolean)
+ */
+ public static <T> T defer(final Entity entity, Supplier<T> task, boolean yielding) {
+ return defer(task, yielding);
+ }
+
+ /**
+ * @see #defer(Supplier, boolean)
+ */
+ public static void defer(Runnable task, boolean yielding) {
+ // Current implementation uses ServerThreadDeferral
+ ServerThreadDeferral.defer(task, yielding);
+ }
+
+ /**
+ * Defers the given {@code task} to any {@link TickThread}, and yields until it has finished.
+ * If this thread is a {@link TickThread}, the task will be executed right away.
+ * <br>
+ * 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.
+ * <br>
+ * 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.
+ * <br>
+ * If this thread is already an appropriate thread to run the task on, the task is performed on this thread.
+ *
+ * @param task The task to run.
+ * @param yielding Whether the task is potentially yielding.
+ */
+ public static <T> T defer(Supplier<T> task, boolean yielding) {
+ // Current implementation uses ServerThreadDeferral
+ return ServerThreadDeferral.defer(task, yielding);
+ }
+
+ /**
+ * An executor for deferring potentially yielding, tasks to the main thread,
+ * where {@link Executor#execute} calls {@link #defer}.
+ */
+ public static final ExecutorService yieldingExecutor = new UnterminableExecutorService() {
+
+ @Override
+ public void execute(@NotNull Runnable runnable) {
+ defer(runnable, true);
+ }
+
+ };
+
+ /**
+ * An executor for deferring yield-free, tasks to the main thread,
+ * where {@link Executor#execute} calls {@link #defer}.
+ */
+ public static final ExecutorService freeExecutor = new UnterminableExecutorService() {
+
+ @Override
+ public void execute(@NotNull Runnable runnable) {
+ defer(runnable, false);
+ }
+
+ };
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/thread/pooled/AbstractYieldingSignallableThreadPool.java b/src/main/java/org/galemc/gale/executor/thread/pooled/AbstractYieldingSignallableThreadPool.java
new file mode 100644
index 0000000000000000000000000000000000000000..b939e9d227650dc2d321cfe3abfaaff3ed1801d6
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/thread/pooled/AbstractYieldingSignallableThreadPool.java
@@ -0,0 +1,31 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.thread.pooled;
+
+import org.galemc.gale.executor.thread.AbstractYieldingThread;
+import org.galemc.gale.executor.thread.wait.SignalReason;
+import org.galemc.gale.executor.thread.wait.WaitingThreadSet;
+
+/**
+ * An interface for thread pools of {@link AbstractYieldingThread}s.
+ * This thread pool can be signalled when new tasks for its threads are added.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+public interface AbstractYieldingSignallableThreadPool {
+
+ /**
+ * Attempts to signal one thread in this pool that is waiting for new tasks.
+ *
+ * @return Whether a thread was signalled (this return value is always accurate).
+ *
+ * @see WaitingThreadSet#pollAndSignal
+ */
+ boolean signalNewTasks(SignalReason reason,boolean yielding);
+
+ /**
+ * @return The number of threads in this pool that are in the {@link Thread.State#RUNNABLE} state.
+ */
+ int getRunnableThreadCount();
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/thread/pooled/AsyncThread.java b/src/main/java/org/galemc/gale/executor/thread/pooled/AsyncThread.java
new file mode 100644
index 0000000000000000000000000000000000000000..7fff5afd87ab9d4aeb0c80d0fced6098569ad1bb
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/thread/pooled/AsyncThread.java
@@ -0,0 +1,39 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.thread.pooled;
+
+import org.galemc.gale.executor.annotation.YieldFree;
+import org.galemc.gale.executor.annotation.thread.AnyThreadSafe;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * This class allows using <code>instanceof</code> to quickly check if the current thread is a {@link AsyncThread}
+ * and provides some thread-local data.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+public final class AsyncThread extends ConditionalSurplusThread {
+
+ AsyncThread(AsyncThreadPool pool, int index) {
+ super(pool, index);
+ }
+
+ /**
+ * @return The current thread if it is a {@link AsyncThread}, or null otherwise.
+ */
+ @AnyThreadSafe
+ @YieldFree
+ public static @Nullable AsyncThread currentAsyncThread() {
+ return Thread.currentThread() instanceof AsyncThread asyncThread ? asyncThread : null;
+ }
+
+ /**
+ * @return Whether the current thread is a {@link AsyncThread}.
+ */
+ @AnyThreadSafe
+ @YieldFree
+ public static boolean isAsyncThread() {
+ return Thread.currentThread() instanceof AsyncThread;
+ }
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/thread/pooled/AsyncThreadPool.java b/src/main/java/org/galemc/gale/executor/thread/pooled/AsyncThreadPool.java
new file mode 100644
index 0000000000000000000000000000000000000000..b86516e034c920f1c551764cd5cc2cc338ce5535
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/thread/pooled/AsyncThreadPool.java
@@ -0,0 +1,66 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.thread.pooled;
+
+import io.papermc.paper.util.TickThread;
+import net.minecraft.server.MinecraftServer;
+import org.galemc.gale.executor.queue.AbstractTaskQueue;
+import org.galemc.gale.executor.queue.BaseTaskQueues;
+
+/**
+ * A {@link ConditionalSurplusThreadPool} with threads that
+ * can perform tasks that must be executed on an {@link AsyncThread}.
+ * These tasks could in principle be executed on a {@link ServerThread}, {@link TickThread} or
+ * {@link TickAssistThread}, but should not be, because these tasks can take a long time and may block the server
+ * from progressing to the next tick if performed on one of these types of threads.
+ * <br>
+ * This pool intends to keep {@link #targetParallelism} threads active at any time,
+ * but subtracts 1 for each active {@link ServerThread}, {@link TickThread} or {@link TickAssistThread}.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+public final class AsyncThreadPool extends ConditionalSurplusThreadPool<AsyncThread> {
+
+ public static final String targetParallelismEnvironmentVariable = "gale.threads.async";
+
+ public static final int ASYNC_THREAD_PRIORITY = Integer.getInteger("gale.thread.priority.async", 6);
+
+ public static final int MAXIMUM_YIELD_DEPTH = Integer.getInteger("gale.maxyielddepth.async", 100);
+
+ /**
+ * The target number of threads that will be actively in use by this pool.
+ * Any active threads with higher priority will be dynamically subtracted from this target.
+ * <br>
+ * This value is always positive.
+ * <br>
+ * Determined by {@link TargetParallelism#computeTargetParallelism(String)}, which can be overridden using
+ * the environment variable {@link #targetParallelismEnvironmentVariable}.
+ */
+ public static final int targetParallelism = TargetParallelism.computeTargetParallelism(targetParallelismEnvironmentVariable);
+
+ public static final AsyncThreadPool instance = new AsyncThreadPool();
+
+ private AsyncThreadPool() {
+ super(new AsyncThread[0], "Base Async Thread", new AbstractTaskQueue[] {
+ // The cleaner queue has high priority because it releases resources back to a pool, thereby saving memory
+ BaseTaskQueues.cleaner,
+ BaseTaskQueues.scheduledAsync
+ }, MAXIMUM_YIELD_DEPTH);
+ }
+
+ @Override
+ public AsyncThread createThread(int index) {
+ AsyncThread thread = new AsyncThread(this, index);
+ thread.setPriority(ASYNC_THREAD_PRIORITY);
+ return thread;
+ }
+
+ @Override
+ protected int computeIntendedActiveThreadCount() {
+ if (!MinecraftServer.isConstructed) {
+ return targetParallelism;
+ }
+ return targetParallelism - ServerThreadPool.instance.getRunnableThreadCount() - TickAssistThreadPool.instance.getRunnableThreadCount();
+ }
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/thread/pooled/ConditionalSurplusThread.java b/src/main/java/org/galemc/gale/executor/thread/pooled/ConditionalSurplusThread.java
new file mode 100644
index 0000000000000000000000000000000000000000..16061701d6df1a372d89046fd2065915892b214f
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/thread/pooled/ConditionalSurplusThread.java
@@ -0,0 +1,102 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.thread.pooled;
+
+import org.galemc.gale.executor.annotation.thread.AnyThreadSafe;
+import org.galemc.gale.executor.annotation.YieldFree;
+import org.galemc.gale.executor.thread.BaseYieldingThread;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A member thread of a {@link ConditionalSurplusThreadPool}.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+@AnyThreadSafe
+@YieldFree
+public abstract class ConditionalSurplusThread extends BaseYieldingThread {
+
+ private final ConditionalSurplusThreadPool<?> pool;
+
+ /**
+ * Prevent race conditions in checks that are done before changing the active thread count, to make sure
+ * a thread does not check a condition in order to change the active thread count, and the condition is changed
+ * and the active thread count changed by another thread before the thread can initiate the change itself.
+ * <br>
+ * This value must always be switched appropriately <i>before</i> the active thread count is changed.
+ */
+ public final AtomicBoolean isCountedInPoolActiveThreadCount = new AtomicBoolean();
+
+ /**
+ * The auto-incremented 0-index index this thread has within this pool.
+ */
+ final int index;
+
+ protected ConditionalSurplusThread(ConditionalSurplusThreadPool<?> pool, int index) {
+ super(BaseYieldingThread::getCurrentBaseYieldingThreadAndRunForever, pool.threadName + " " + index, pool.taskQueues, pool.threadsWaitingForYieldingTasks, pool.threadsWaitingForFreeTasks, true, pool.maximumYieldDepth);
+ this.pool = pool;
+ this.index = index;
+ }
+
+ @Override
+ protected boolean wantsToPoll() {
+ // If this thread is an excess thread, just pause here
+ for (;;) {
+ // Set isCountedInPoolActiveThreadCount to false in anticipation
+ if (this.isCountedInPoolActiveThreadCount.getAndSet(false)) {
+ if (this.pool.decrementActiveThreadCountIfLargerThanIntended()) {
+ return false;
+ }
+ // Undo the earlier isCountedInPoolActiveThreadCount set
+ if (!this.isCountedInPoolActiveThreadCount.getAndSet(true)) {
+ return true;
+ }
+ // Strange race condition occurrence
+ continue;
+ }
+ // Strange race condition occurrence
+ return false;
+ }
+ }
+
+ @Override
+ public boolean willWaitAfterPollFailure() {
+ if (this.isRestrictedDueToYieldDepth) {
+ if (this.isCountedInPoolActiveThreadCount.getAndSet(false)) {
+ /*
+ This thread will not be attempted to be activated by the updateActiveThreads caused below,
+ because it only activates threads for which isRestrictedDueToYieldDepth is false.
+ */
+ this.pool.decrementActiveThreadCountAndUpdateActiveThreads();
+ }
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void wokeUpAfterNonBecomeAllowedToPollSignal() {
+ /*
+ If the signal was becomesAllowedToPollSignalReason, the activeThreadCount will already have been
+ incremented in updateActiveThreads, but the signal is different. If this thread woke up and was not
+ registered for tasks before, that means it went from inactive to active.
+ */
+ if (!this.isRegisteredForNewTasks) {
+ if (!this.isCountedInPoolActiveThreadCount.getAndSet(true)) {
+ this.pool.incrementActiveThreadCountAndUpdateActiveThreads();
+ }
+ }
+ }
+
+ @Override
+ protected void preBlockThread() {
+ this.pool.incrementRunnableThreadCount();
+ }
+
+ @Override
+ protected void postBlockThread() {
+ this.pool.decrementRunnableThreadCount();
+ }
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/thread/pooled/ConditionalSurplusThreadPool.java b/src/main/java/org/galemc/gale/executor/thread/pooled/ConditionalSurplusThreadPool.java
new file mode 100644
index 0000000000000000000000000000000000000000..22c3ad33a784cb8d6bcdcb8c787029f4922f6123
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/thread/pooled/ConditionalSurplusThreadPool.java
@@ -0,0 +1,408 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.thread.pooled;
+
+import org.galemc.gale.concurrent.Mutex;
+import org.galemc.gale.executor.annotation.Access;
+import org.galemc.gale.executor.annotation.thread.AnyThreadSafe;
+import org.galemc.gale.executor.annotation.Guarded;
+import org.galemc.gale.executor.annotation.YieldFree;
+import org.galemc.gale.executor.lock.YieldingLock;
+import org.galemc.gale.executor.queue.AbstractTaskQueue;
+import org.galemc.gale.executor.thread.BaseYieldingThread;
+import org.galemc.gale.executor.thread.wait.IndexArrayWaitingThreadSet;
+import org.galemc.gale.executor.thread.wait.SignalReason;
+import org.galemc.gale.executor.thread.wait.WaitingThreadSet;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A pool of threads that can perform specific types of tasks.
+ * The threads in this thread pool perform an endless loop, polling and executing tasks.
+ * <br>
+ * This thread pool has a dynamic amount of threads that are intended to be polling or running tasks at a given moment:
+ * we refer to such threads as active below. If a thread is listening for new tasks to be added, and it is not
+ * restricted (i.e. it could accept any task any newly spawned thread in this pool could accept) then it counts as
+ * active too (since it will be signalled as soon as new tasks are added and there would be no use in replacing it
+ * with a newly spawned thread).
+ * If the number of active threads becomes lower than this value, surplus threads become allowed, and are signalled
+ * (and will be spawned and added to the thread pool if not present yet) to start polling tasks, in order to
+ * reach the intended amount of active threads again.
+ * If the number of active threads becomes higher than this value, some excess threads will not be able
+ * to poll new tasks until allowed again. Potentially, The threads will stay in the {@link Thread.State#RUNNABLE}
+ * state until they finish a task they were already executing.
+ * <br>
+ * While a thread is not allowed to poll new tasks, it can still be signalled for other reasons, such as a
+ * {@link YieldingLock} they were waiting for being released.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+@AnyThreadSafe
+@YieldFree
+public abstract class ConditionalSurplusThreadPool<T extends ConditionalSurplusThread> implements AbstractYieldingSignallableThreadPool {
+
+ /**
+ * The name for newly spawned threads. The index of the thread within this pool will be appended.
+ */
+ final String threadName;
+
+ /**
+ * The value of {@link BaseYieldingThread#taskQueues} that will be passed to member threads.
+ */
+ final AbstractTaskQueue[] taskQueues;
+
+ /**
+ * A {@link WaitingThreadSet} of member threads waiting for potentially yielding tasks to be added.
+ */
+ final WaitingThreadSet threadsWaitingForYieldingTasks = new WaitingMemberThreadSet();
+
+ /**
+ * A {@link WaitingThreadSet} of member threads waiting for yield-free tasks to be added.
+ */
+ final WaitingThreadSet threadsWaitingForFreeTasks = new WaitingMemberThreadSet() {
+
+ @Override
+ public boolean pollAndSignal(SignalReason reason) {
+ if (super.pollAndSignal(reason)) {
+ return true;
+ }
+ /*
+ Waiting for potentially yielding tasks implies waiting for yield-free tasks,
+ so we signal threads waiting for potentially yielding tasks too.
+ */
+ return threadsWaitingForYieldingTasks.pollAndSignal(reason);
+ }
+
+ };
+
+ /**
+ * The value of {@link BaseYieldingThread#maximumYieldDepth} that will be passed to member threads.
+ */
+ final int maximumYieldDepth;
+
+ /**
+ * An array of the threads in this pool, indexed by their {@link ConditionalSurplusThread#index}.
+ */
+ @Guarded(value = "#threadsLock", fieldAccess = Access.WRITE)
+ private volatile T[] threads;
+
+ private final Mutex threadsLock = Mutex.create();
+
+ /**
+ * The last computed number of intended active threads.
+ * <br>
+ * This value is updated in {@link #updateActiveThreads()}, where its value will be based on
+ * {@link #computeIntendedActiveThreadCount}.
+ */
+ public volatile int lastComputedIntendedActiveThreadCount;
+
+ /**
+ * The actual number of threads that are active now, i.e. the number of threads that are either runnable and
+ * polling or executing tasks, or not restricted and awaiting new tasks to be added.
+ */
+ private final AtomicInteger activeThreadCount = new AtomicInteger();
+
+ /**
+ * The number of threads that are in the {@link Thread.State#NEW}, {@link Thread.State#RUNNABLE} or
+ * {@link Thread.State#TERMINATED} states.
+ */
+ private final AtomicInteger runnableThreadCount = new AtomicInteger();
+
+ /**
+ * Whether a call to {@link #updateActiveThreads} is ongoing.
+ */
+ private final AtomicBoolean updateActiveThreadsIsOngoing = new AtomicBoolean();
+
+ /**
+ * Whether to repeat the outermost loop within {@link #updateActiveThreads} due to a second call to
+ * {@link #updateActiveThreads} happening while {@link #updateActiveThreadsIsOngoing} was already true.
+ */
+ private volatile boolean repeatUpdateActiveThreadLoop = false;
+
+ protected ConditionalSurplusThreadPool(T[] emptyThreadArray, String threadName, AbstractTaskQueue[] taskQueues, int maximumYieldDepth) {
+ this.threads = emptyThreadArray;
+ this.threadName = threadName;
+ this.taskQueues = taskQueues;
+ for (AbstractTaskQueue queue : this.taskQueues) {
+ queue.setThreadPool(this);
+ }
+ this.maximumYieldDepth = maximumYieldDepth;
+ this.intendedActiveThreadCountMayHaveChanged();
+ this.updateActiveThreads();
+ }
+
+ /**
+ * @return A newly constructed member thread with the given index within this pool.
+ */
+ public abstract T createThread(int index);
+
+ /**
+ * @return The number of threads that are intended to be active now.
+ */
+ protected abstract int computeIntendedActiveThreadCount();
+
+ @Override
+ public boolean signalNewTasks(SignalReason reason, boolean yielding) {
+ // First attempt without updating the active threads, then attempt after updating the active threads
+ for (int attempt = 0; attempt < 2; attempt++) {
+ if (attempt == 1) {
+ this.updateActiveThreads();
+ }
+ for (T thread : this.threads) {
+ if (thread.isWaiting && thread.isRegisteredForNewTasks) {
+ if (thread.signal(reason)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public int getRunnableThreadCount() {
+ return this.runnableThreadCount.get();
+ }
+
+ public void incrementRunnableThreadCount() {
+ this.runnableThreadCount.incrementAndGet();
+ }
+
+ public void decrementRunnableThreadCount() {
+ this.runnableThreadCount.decrementAndGet();
+ }
+
+ public T getThreadByIndex(int index) {
+ return this.threads[index];
+ }
+
+ public void decrementActiveThreadCountAndUpdateActiveThreads() {
+ this.activeThreadCount.decrementAndGet();
+ this.updateActiveThreads();
+ }
+
+ public void incrementActiveThreadCountAndUpdateActiveThreads() {
+ this.activeThreadCount.incrementAndGet();
+ this.updateActiveThreads();
+ }
+
+ /**
+ * This implementation is based on the implementation of {@link AtomicInteger#updateAndGet}.
+ *
+ * @return Whether {@link #activeThreadCount} was updated by this method.
+ */
+ public boolean decrementActiveThreadCountIfLargerThanIntended() {
+ for (;;) {
+ int prev = this.activeThreadCount.get();
+ if (prev <= this.lastComputedIntendedActiveThreadCount) {
+ return false;
+ }
+ int next = prev - 1;
+ if (this.activeThreadCount.weakCompareAndSetVolatile(prev, next)) {
+ return true;
+ }
+ }
+ }
+
+ /**
+ * This implementation is based on the implementation of {@link AtomicInteger#updateAndGet}.
+ *
+ * @return Whether {@link #activeThreadCount} was updated by this method.
+ */
+ public boolean incrementActiveThreadCountIfSmallerThanIntended() {
+ for (;;) {
+ int prev = this.activeThreadCount.get();
+ if (prev >= this.lastComputedIntendedActiveThreadCount) {
+ return false;
+ }
+ int next = prev + 1;
+ if (this.activeThreadCount.weakCompareAndSetVolatile(prev, next)) {
+ return true;
+ }
+ }
+ }
+
+ /**
+ * Re-computes the number of threads that should be active, and modifies the thread pool accordingly. May add
+ * new surplus threads or allow or disallow threads to listen for new tasks.
+ * <br>
+ * Must be called after the value of {@link #computeIntendedActiveThreadCount()} may have changed, and must
+ * be called after the value of {@link #activeThreadCount} has changed.
+ */
+ void updateActiveThreads() {
+ if (!updateActiveThreadsIsOngoing.getAndSet(true)) {
+ try {
+ // Keep (de-)activating threads while necessary
+ for (;;) {
+ int previousActiveThreadCount = this.activeThreadCount.get();
+ if (previousActiveThreadCount == this.lastComputedIntendedActiveThreadCount) {
+ // The number of active threads is perfect
+ return;
+ }
+ // Remember if we made any changes
+ boolean madeChanges = false;
+ // There is no point in activating more threads if there are no taskss
+ boolean previousHadTasksForNewThreads = AbstractTaskQueue.taskQueuesHaveTasks(this.taskQueues);
+ if (previousActiveThreadCount < this.lastComputedIntendedActiveThreadCount && previousHadTasksForNewThreads) {
+ // Allow some threads to listen to new tasks, or signal them if there are already tasks available
+ for (byte alsoTryZeroYieldDepth = 0; alsoTryZeroYieldDepth < 2; alsoTryZeroYieldDepth++) {
+ for (T thread : this.threads) {
+ // Speculatively verify that the thread is waiting and not registered for new tasks
+ if (thread.isWaiting && !thread.isRegisteredForNewTasks) {
+ // Speculatively check that the thread is non-restricted
+ if (!thread.isRestrictedDueToYieldDepth) {
+ // First attempt to choose a thread that already has a positive yield depth
+ if (alsoTryZeroYieldDepth == 1 || thread.yieldDepth > 0) {
+ // Continue if the thread is successfully marked as counted active
+ if (!thread.isCountedInPoolActiveThreadCount.getAndSet(true)) {
+ boolean mustUndoSetCountedInPoolActiveThreadCount = true;
+ try {
+ // Continue if the active thread count is successfully incremented
+ if (this.incrementActiveThreadCountIfSmallerThanIntended()) {
+ // Activate the thread if possible
+ if (!thread.activateIfNotListeningForTasksAndNotRestricted()) {
+ // If not possible, undo the active thread count increment
+ if (this.activeThreadCount.decrementAndGet() == this.lastComputedIntendedActiveThreadCount) {
+ // The number of active threads matches, return
+ return;
+ }
+ } else {
+ madeChanges = true;
+ mustUndoSetCountedInPoolActiveThreadCount = false;
+ if (this.activeThreadCount.get() == this.lastComputedIntendedActiveThreadCount) {
+ // The number of active threads matches, return
+ return;
+ }
+ }
+ }
+ } finally {
+ if (mustUndoSetCountedInPoolActiveThreadCount) {
+ // Undo setting isCountedInPoolActiveThreadCount
+ thread.isCountedInPoolActiveThreadCount.set(false);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ // Add more active threads by adding threads to the thread pool
+ int surplusThreadsToAdd = this.lastComputedIntendedActiveThreadCount - this.activeThreadCount.get();
+ if (surplusThreadsToAdd == 0) {
+ // The number of active threads matches, return
+ return;
+ } else if (surplusThreadsToAdd > 0) {
+ // We need to add surplus threads
+ //noinspection StatementWithEmptyBody
+ while (!this.threadsLock.tryAcquire()) ;
+ try {
+ surplusThreadsToAdd = this.lastComputedIntendedActiveThreadCount - this.activeThreadCount.get();
+ int oldThreadsLength = this.threads.length;
+ int newThreadsLength = oldThreadsLength + surplusThreadsToAdd;
+ T[] newThreads = Arrays.copyOf(this.threads, newThreadsLength);
+ for (int i = oldThreadsLength; i < newThreadsLength; i++) {
+ newThreads[i] = createThread(i);
+ }
+ madeChanges = true;
+ this.threads = newThreads;
+ for (int i = oldThreadsLength; i < newThreadsLength; i++) {
+ if (!newThreads[i].isCountedInPoolActiveThreadCount.getAndSet(true)) {
+ this.activeThreadCount.incrementAndGet();
+ }
+ this.incrementRunnableThreadCount();
+ newThreads[i].start();
+ }
+ } finally {
+ this.threadsLock.release();
+ }
+ }
+ } else {
+ // Stop some threads from listening to new tasks
+ for (byte alsoTryPositiveYieldDepth = 0; alsoTryPositiveYieldDepth < 2; alsoTryPositiveYieldDepth++) {
+ for (T thread : this.threads) {
+ // Speculatively verify that the thread is waiting and registered for new tasks
+ if (thread.isWaiting && thread.isRegisteredForNewTasks) {
+ // First attempt to choose a thread that has a yield depth of zero
+ if (alsoTryPositiveYieldDepth == 1 || thread.yieldDepth == 0) {
+ // Continue if the thread is successfully unmarked as counted active
+ if (thread.isCountedInPoolActiveThreadCount.getAndSet(false)) {
+ boolean mustUndoSetCountedInPoolActiveThreadCount = true;
+ try {
+ // Continue if the active thread count is successfully incremented
+ if (this.decrementActiveThreadCountIfLargerThanIntended()) {
+ // Activate the thread if possible
+ if (!thread.deactivateIfListeningForTasks()) {
+ // If not possible, undo the active thread count decrement
+ if (this.activeThreadCount.incrementAndGet() == this.lastComputedIntendedActiveThreadCount) {
+ // The number of active threads matches, return
+ return;
+ }
+ } else {
+ madeChanges = true;
+ mustUndoSetCountedInPoolActiveThreadCount = false;
+ if (this.activeThreadCount.get() == this.lastComputedIntendedActiveThreadCount) {
+ // The number of active threads matches, return
+ return;
+ }
+ }
+ }
+ } finally {
+ if (mustUndoSetCountedInPoolActiveThreadCount) {
+ // Undo setting isCountedInPoolActiveThreadCount
+ thread.isCountedInPoolActiveThreadCount.set(true);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ // Stop if nothing interesting changed
+ if (!this.repeatUpdateActiveThreadLoop && !madeChanges && this.activeThreadCount.get() == previousActiveThreadCount && (previousHadTasksForNewThreads || !AbstractTaskQueue.taskQueuesHaveTasks(this.taskQueues))) {
+ return;
+ }
+ this.repeatUpdateActiveThreadLoop = false;
+ }
+ } finally {
+ this.updateActiveThreadsIsOngoing.set(false);
+ }
+ } else {
+ this.repeatUpdateActiveThreadLoop = true;
+ }
+ }
+
+ /**
+ * To be called when the result of {@link #computeIntendedActiveThreadCount()} may have changed.
+ */
+ public void intendedActiveThreadCountMayHaveChanged() {
+ int previousLastComputedIntendedActiveThreadCount = this.lastComputedIntendedActiveThreadCount;
+ int newComputedIntendedActiveThreadCount = this.computeIntendedActiveThreadCount();
+ if (previousLastComputedIntendedActiveThreadCount != newComputedIntendedActiveThreadCount) {
+ this.lastComputedIntendedActiveThreadCount = newComputedIntendedActiveThreadCount;
+ this.updateActiveThreads();
+ }
+ }
+
+ /**
+ * A {@link WaitingThreadSet} of member threads waiting for tasks to be added.
+ */
+ public class WaitingMemberThreadSet extends IndexArrayWaitingThreadSet<T> {
+
+ @Override
+ protected int getThreadIndex(T thread) {
+ return thread.index;
+ }
+
+ @Override
+ protected @NotNull T getThreadByIndex(int index) {
+ return ConditionalSurplusThreadPool.this.threads[index];
+ }
+
+ }
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/thread/pooled/ServerThread.java b/src/main/java/org/galemc/gale/executor/thread/pooled/ServerThread.java
new file mode 100644
index 0000000000000000000000000000000000000000..048c33d025fcd0b12780c08e53498a10380136e5
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/thread/pooled/ServerThread.java
@@ -0,0 +1,77 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.thread.pooled;
+
+import io.papermc.paper.util.TickThread;
+import net.minecraft.server.MinecraftServer;
+import org.galemc.gale.executor.thread.BaseYieldingThread;
+import org.jetbrains.annotations.NotNull;
+import org.spigotmc.WatchdogThread;
+
+/**
+ * A {@link TickThread} that provides an implementation for {@link BaseYieldingThread},
+ * that is shared between the {@link MinecraftServer#serverThread} and {@link WatchdogThread#instance}.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+public class ServerThread extends TickThread {
+
+ /**
+ * Whether this thread is currently blocked, specifically meaning: whether this thread has at some earlier point
+ * been in the {@link Thread.State#RUNNABLE} state, but is now (or about to be, or just was) in a state that is
+ * not {@link Thread.State#NEW}, {@link Thread.State#RUNNABLE} or {@link Thread.State#TERMINATED}.
+ */
+ public volatile boolean isBlocked;
+
+ protected ServerThread(final String name) {
+ super(name);
+ }
+
+ protected ServerThread(final Runnable run, final String name) {
+ super(run, name);
+ }
+
+ @Override
+ protected boolean wantsToPoll() {
+ return true;
+ }
+
+ @Override
+ protected boolean willWaitAfterPollFailure() {
+ return true;
+ }
+
+ @Override
+ protected void wokeUpAfterNonBecomeAllowedToPollSignal() {}
+
+ @Override
+ protected void preBlockThread() {
+ this.isBlocked = true;
+ TickAssistThreadPool.instance.intendedActiveThreadCountMayHaveChanged();
+ AsyncThreadPool.instance.intendedActiveThreadCountMayHaveChanged();
+ }
+
+ @Override
+ protected void postBlockThread() {
+ this.isBlocked = false;
+ TickAssistThreadPool.instance.intendedActiveThreadCountMayHaveChanged();
+ AsyncThreadPool.instance.intendedActiveThreadCountMayHaveChanged();
+ }
+
+ /**
+ * This method must not be called while {@link MinecraftServer#isConstructed} is false.
+ *
+ * @return The global {@link ServerThread} instance, which is either
+ * {@link MinecraftServer#serverThread}, or {@link WatchdogThread#instance} while the server is shutting
+ * down and the {@link WatchdogThread} was responsible.
+ */
+ public static @NotNull ServerThread getInstance() {
+ if (MinecraftServer.SERVER.hasStopped) {
+ if (MinecraftServer.SERVER.shutdownThread == WatchdogThread.instance) {
+ return WatchdogThread.instance;
+ }
+ }
+ return MinecraftServer.serverThread;
+ }
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/thread/pooled/ServerThreadPool.java b/src/main/java/org/galemc/gale/executor/thread/pooled/ServerThreadPool.java
new file mode 100644
index 0000000000000000000000000000000000000000..ec0b6055cd653ef4b7f9e86267a563fe857d8942
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/thread/pooled/ServerThreadPool.java
@@ -0,0 +1,95 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.thread.pooled;
+
+import io.papermc.paper.util.TickThread;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.entity.Entity;
+import org.galemc.gale.executor.queue.AbstractTaskQueue;
+import org.galemc.gale.executor.queue.BaseTaskQueues;
+import org.galemc.gale.executor.thread.wait.SignalReason;
+import org.galemc.gale.executor.thread.wait.WaitingServerThreadSet;
+import org.galemc.gale.executor.thread.wait.WaitingThreadSet;
+
+/**
+ * An implementation of {@link AbstractYieldingSignallableThreadPool} for {@link ServerThread}s.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+public final class ServerThreadPool implements AbstractYieldingSignallableThreadPool {
+
+ /**
+ * The queues that {@link ServerThread}s poll from.
+ * <br>
+ * Some parts of the server can only be safely accessed by one thread at a time.
+ * If they can not be guarded by a lock (or if this is not desired,
+ * because if a ticking thread would need to acquire this lock it would block it),
+ * then these parts of the code are typically deferred to the server thread.
+ * Based on the current use of the {@link TickThread} class, particularly given the existence of
+ * {@link TickThread#isTickThreadFor(Entity)} and {@link TickThread#isTickThreadFor(ServerLevel, int, int)},
+ * we can deduce that future support for performing some of these actions in parallel is planned.
+ * In such a case, some server thread tasks may become tasks that must be
+ * executed on an appropriate {@link TickThread}.
+ * In that case, the queues below should be changed so that the server thread and any of the
+ * ticking threads poll from queues that contain tasks appropriate for them.
+ * For example, {@link BaseTaskQueues#deferredToUniversalTickThread} would be for tasks that can run
+ * on any ticking thread, and additional queues would need to be added concerning a specific
+ * subject (like an entity or chunk) with tasks that will be run on whichever ticking thread is the
+ * ticking thread for that subject at the time of polling.
+ */
+ public static final AbstractTaskQueue[] taskQueues = {
+ BaseTaskQueues.deferredToServerThread,
+ BaseTaskQueues.serverThreadTick,
+ BaseTaskQueues.anyTickScheduledServerThread,
+ BaseTaskQueues.allLevelsScheduledChunkCache,
+ BaseTaskQueues.allLevelsScheduledTickThreadChunk
+ };
+
+ /**
+ * A {@link WaitingThreadSet} of {@link ServerThread}s waiting for potentially yielding tasks to be added.
+ */
+ public final static WaitingThreadSet threadsWaitingForYieldingTasks = new WaitingServerThreadSet();
+
+ /**
+ * A {@link WaitingThreadSet} of {@link ServerThread}s waiting for yield-free tasks to be added.
+ */
+ public final static WaitingThreadSet threadsWaitingForFreeTasks = new WaitingServerThreadSet() {
+
+ @Override
+ public boolean pollAndSignal(SignalReason reason) {
+ if (super.pollAndSignal(reason)) {
+ return true;
+ }
+ /*
+ Waiting for potentially yielding tasks implies waiting for yield-free tasks,
+ so we signal threads waiting for potentially yielding tasks too.
+ */
+ return threadsWaitingForYieldingTasks.pollAndSignal(reason);
+ }
+
+ };
+
+ public static final ServerThreadPool instance = new ServerThreadPool();
+
+ private ServerThreadPool() {
+ for (AbstractTaskQueue queue : taskQueues) {
+ queue.setThreadPool(this);
+ }
+ }
+
+ @Override
+ public boolean signalNewTasks(SignalReason reason, boolean yielding) {
+ ServerThread serverThread = ServerThread.getInstance();
+ if (!yielding || !serverThread.isRestrictedDueToYieldDepth) {
+ return serverThread.signal(reason);
+ }
+ return false;
+ }
+
+ @Override
+ public int getRunnableThreadCount() {
+ return ServerThread.getInstance().isBlocked ? 0 : 1;
+ }
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/thread/pooled/TargetParallelism.java b/src/main/java/org/galemc/gale/executor/thread/pooled/TargetParallelism.java
new file mode 100644
index 0000000000000000000000000000000000000000..9a4c05bb45b6bef224e42883d7fd6ee7309bf63c
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/thread/pooled/TargetParallelism.java
@@ -0,0 +1,75 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.thread.pooled;
+
+import org.galemc.gale.util.CPUCoresEstimation;
+
+/**
+ * A utility class to determine the target parallelism for thread pools by the number of available CPU cores.
+ * <br>
+ * The value is currently automatically determined according to the following table:
+ * <table>
+ * <tr><th>system cores</th><th>cores spared</th></tr>
+ * <tr><td>&#8804; 3</td><td>0</td></tr>
+ * <tr><td>[4, 14]</td><td>1</td></tr>
+ * <tr><td>[15, 23]</td><td>2</td></tr>
+ * <tr><td>[24, 37]</td><td>3</td></tr>
+ * <tr><td>[38, 54]</td><td>4</td></tr>
+ * <tr><td>[55, 74]</td><td>5</td></tr>
+ * <tr><td>[75, 99]</td><td>6</td></tr>
+ * <tr><td>[100, 127]</td><td>7</td></tr>
+ * <tr><td>[128, 158]</td><td>8</td></tr>
+ * <tr><td>[159, 193]</td><td>9</td></tr>
+ * <tr><td>[194, 232]</td><td>10</td></tr>
+ * <tr><td>[233, 274]</td><td>11</td></tr>
+ * <tr><td>&#8805; 275</td><td>12</td></tr>
+ * </table>
+ * Then <code>target parallelism = system cores - cores spared</code>.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+public final class TargetParallelism {
+
+ private TargetParallelism() {}
+
+ static int computeTargetParallelism(String environmentVariable) {
+ int parallelismByEnvironmentVariable = Integer.getInteger(environmentVariable, -1);
+ int targetParallelismBeforeSetAtLeastOne;
+ if (parallelismByEnvironmentVariable >= 0) {
+ targetParallelismBeforeSetAtLeastOne = parallelismByEnvironmentVariable;
+ } else {
+ int systemCores = CPUCoresEstimation.get();
+ int coresSpared;
+ if (systemCores <= 3) {
+ coresSpared = 0;
+ } else if (systemCores <= 14) {
+ coresSpared = 1;
+ } else if (systemCores <= 23) {
+ coresSpared = 2;
+ } else if (systemCores <= 37) {
+ coresSpared = 3;
+ } else if (systemCores <= 54) {
+ coresSpared = 4;
+ } else if (systemCores <= 74) {
+ coresSpared = 5;
+ } else if (systemCores <= 99) {
+ coresSpared = 6;
+ } else if (systemCores <= 127) {
+ coresSpared = 7;
+ } else if (systemCores <= 158) {
+ coresSpared = 8;
+ } else if (systemCores <= 193) {
+ coresSpared = 9;
+ } else if (systemCores <= 232) {
+ coresSpared = 10;
+ } else if (systemCores <= 274) {
+ coresSpared = 11;
+ } else {
+ coresSpared = 12;
+ }
+ targetParallelismBeforeSetAtLeastOne = systemCores - coresSpared;
+ }
+ return Math.max(1, targetParallelismBeforeSetAtLeastOne);
+ }
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/thread/pooled/TickAssistThread.java b/src/main/java/org/galemc/gale/executor/thread/pooled/TickAssistThread.java
new file mode 100644
index 0000000000000000000000000000000000000000..da9265f13dea8a0b971402fa7e09d30f44f38be4
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/thread/pooled/TickAssistThread.java
@@ -0,0 +1,39 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.thread.pooled;
+
+import org.galemc.gale.executor.annotation.thread.AnyThreadSafe;
+import org.galemc.gale.executor.annotation.YieldFree;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * This class allows using <code>instanceof</code> to quickly check if the current thread is a {@link TickAssistThread}
+ * and provides some thread-local data.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+public final class TickAssistThread extends ConditionalSurplusThread {
+
+ TickAssistThread(TickAssistThreadPool pool, int index) {
+ super(pool, index);
+ }
+
+ /**
+ * @return The current thread if it is a {@link TickAssistThread}, or null otherwise.
+ */
+ @AnyThreadSafe
+ @YieldFree
+ public static @Nullable TickAssistThread currentTickAssistThread() {
+ return Thread.currentThread() instanceof TickAssistThread tickAssistThread ? tickAssistThread : null;
+ }
+
+ /**
+ * @return Whether the current thread is a {@link TickAssistThread}.
+ */
+ @AnyThreadSafe
+ @YieldFree
+ public static boolean isTickAssistThread() {
+ return Thread.currentThread() instanceof TickAssistThread;
+ }
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/thread/pooled/TickAssistThreadPool.java b/src/main/java/org/galemc/gale/executor/thread/pooled/TickAssistThreadPool.java
new file mode 100644
index 0000000000000000000000000000000000000000..51a94ffbd039260bdb84ee80131ae18280ce6064
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/thread/pooled/TickAssistThreadPool.java
@@ -0,0 +1,116 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.thread.pooled;
+
+import io.papermc.paper.util.TickThread;
+import net.minecraft.server.MinecraftServer;
+import org.galemc.gale.executor.queue.AbstractTaskQueue;
+import org.galemc.gale.executor.queue.BaseTaskQueues;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A {@link ConditionalSurplusThreadPool} with threads that
+ * can perform tasks that are part of ticking to assist the main ticking thread(s) in doing so.
+ * Such tasks could in principle be executed on a {@link ServerThread} or {@link TickThread}, but should not be,
+ * to keep {@link ServerThread}s and {@link TickThread}s immediately available to run tasks specially for them.
+ * <br>
+ * This pool intends to keep {@link #targetParallelism} threads active at any time,
+ * but subtracts 1 for each active {@link ServerThread} or {@link TickThread}.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+public final class TickAssistThreadPool extends ConditionalSurplusThreadPool<TickAssistThread> {
+
+ public static final String targetParallelismEnvironmentVariable = "gale.threads.tickassist";
+
+ public static final int TICK_ASSIST_THREAD_PRIORITY = Integer.getInteger("gale.thread.priority.tickassist", 7);
+
+ public static final int MAXIMUM_YIELD_DEPTH = Integer.getInteger("gale.maxyielddepth.tickassist", 100);
+
+ /**
+ * The target number of threads that will be actively in use by this pool.
+ * Any active threads with higher priority will be dynamically subtracted from this target.
+ * <br>
+ * This value is always positive.
+ * <br>
+ * Determined by {@link TargetParallelism#computeTargetParallelism(String)}, which can be overridden using
+ * the environment variable {@link #targetParallelismEnvironmentVariable}.
+ */
+ public static final int targetParallelism = TargetParallelism.computeTargetParallelism(targetParallelismEnvironmentVariable);
+
+ /**
+ * This counter makes sure we do not constantly notify {@link AsyncThreadPool} of changes in the
+ * number of threads in this pool during {@link #updateActiveThreads()}.
+ */
+ public static final AtomicInteger doNotNotifyOfThreadCountChangesCounter = new AtomicInteger();
+
+ /**
+ * Whether this pool skipped notifying {@link AsyncThreadPool} due to
+ * {@link #doNotNotifyOfThreadCountChangesCounter}.
+ */
+ public static volatile boolean skippedNotifyOfThreadCountChanges = false;
+
+ public static final TickAssistThreadPool instance;
+ static {
+ // Make sure we do not notify the AsyncThreadPool while instance is not set yet
+ doNotNotifyOfThreadCountChangesCounter.incrementAndGet();
+ instance = new TickAssistThreadPool();
+ if (doNotNotifyOfThreadCountChangesCounter.decrementAndGet() == 0 && skippedNotifyOfThreadCountChanges) {
+ skippedNotifyOfThreadCountChanges = false;
+ AsyncThreadPool.instance.intendedActiveThreadCountMayHaveChanged();
+ }
+ }
+
+ private TickAssistThreadPool() {
+ super(new TickAssistThread[0], "Tick Assist Thread", new AbstractTaskQueue[] {
+ BaseTaskQueues.tickAssist
+ }, MAXIMUM_YIELD_DEPTH);
+ }
+
+ @Override
+ public TickAssistThread createThread(int index) {
+ TickAssistThread thread = new TickAssistThread(this, index);
+ thread.setPriority(TICK_ASSIST_THREAD_PRIORITY);
+ return thread;
+ }
+
+ @Override
+ protected int computeIntendedActiveThreadCount() {
+ if (!MinecraftServer.isConstructed) {
+ return 0;
+ }
+ return targetParallelism - ServerThreadPool.instance.getRunnableThreadCount();
+ }
+
+ @Override
+ public void incrementRunnableThreadCount() {
+ super.incrementRunnableThreadCount();
+ if (doNotNotifyOfThreadCountChangesCounter.get() == 0) {
+ AsyncThreadPool.instance.intendedActiveThreadCountMayHaveChanged();
+ } else {
+ skippedNotifyOfThreadCountChanges = true;
+ }
+ }
+
+ @Override
+ public void decrementRunnableThreadCount() {
+ super.decrementRunnableThreadCount();
+ if (doNotNotifyOfThreadCountChangesCounter.get() == 0) {
+ AsyncThreadPool.instance.intendedActiveThreadCountMayHaveChanged();
+ } else {
+ skippedNotifyOfThreadCountChanges = true;
+ }
+ }
+
+ @Override
+ void updateActiveThreads() {
+ doNotNotifyOfThreadCountChangesCounter.incrementAndGet();
+ super.updateActiveThreads();
+ if (doNotNotifyOfThreadCountChangesCounter.decrementAndGet() == 0 && skippedNotifyOfThreadCountChanges) {
+ skippedNotifyOfThreadCountChanges = false;
+ AsyncThreadPool.instance.intendedActiveThreadCountMayHaveChanged();
+ }
+ }
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/thread/wait/IndexArrayWaitingThreadSet.java b/src/main/java/org/galemc/gale/executor/thread/wait/IndexArrayWaitingThreadSet.java
new file mode 100644
index 0000000000000000000000000000000000000000..a3c11208a831a8e55dfd0cfc6698ea571f602e75
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/thread/wait/IndexArrayWaitingThreadSet.java
@@ -0,0 +1,161 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.thread.wait;
+
+import org.galemc.gale.concurrent.Mutex;
+import org.galemc.gale.executor.annotation.Access;
+import org.galemc.gale.executor.annotation.thread.AnyThreadSafe;
+import org.galemc.gale.executor.annotation.Guarded;
+import org.galemc.gale.executor.annotation.YieldFree;
+import org.galemc.gale.executor.lock.YieldingLock;
+import org.galemc.gale.executor.thread.SignallableThread;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Arrays;
+
+/**
+ * A set of waiting threads. This is used to collect the threads that are all waiting for new work,
+ * e.g. for new tasks to be added to a queue or for a {@link YieldingLock} to be released.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+@AnyThreadSafe
+@YieldFree
+public abstract class IndexArrayWaitingThreadSet<T extends Thread & SignallableThread> implements WaitingThreadSet {
+
+ /**
+ * A set of the indices of the threads in this collection.
+ */
+ @Guarded(value = "#lock", fieldAccess = Access.WRITE)
+ private volatile boolean[] elements = new boolean[0];
+
+ /**
+ * An index in {@link #elements} that is likely to contain {@code true}, or an arbitrary but valid index otherwise.
+ */
+ @Guarded(value = "#lock", fieldAccess = Access.WRITE)
+ private volatile int potentialLowestPresentElementIndex;
+
+ private final Mutex lock = Mutex.create();
+
+ @Override
+ public void add(Thread thread) {
+ @SuppressWarnings("unchecked")
+ int index = this.getThreadIndex((T) thread);
+ // Store field in local variable for non-volatile access
+ boolean[] elements = this.elements;
+ if (elements.length > index && elements[index]) {
+ return;
+ }
+ //noinspection StatementWithEmptyBody
+ while (!this.lock.tryAcquire());
+ try {
+ // Store field in local variable for non-volatile access
+ elements = this.elements;
+ if (elements.length <= index) {
+ this.elements = elements = Arrays.copyOf(elements, index + 1);
+ }
+ elements[index] = true;
+ this.potentialLowestPresentElementIndex = index;
+ } finally {
+ this.lock.release();
+ }
+ }
+
+ @Override
+ public void remove(Thread thread) {
+ @SuppressWarnings("unchecked")
+ int index = this.getThreadIndex((T) thread);
+ // Store field in local variable for non-volatile access
+ boolean[] elements = this.elements;
+ if (elements.length <= index || !elements[index]) {
+ return;
+ }
+ //noinspection StatementWithEmptyBody
+ while (!this.lock.tryAcquire());
+ try {
+ this.elements[index] = false;
+ } finally {
+ this.lock.release();
+ }
+ }
+
+ @Override
+ public boolean pollAndSignal(SignalReason reason) {
+ @Nullable T polled = this.poll();
+ //noinspection SimplifiableConditionalExpression
+ return polled != null ? polled.signal(reason) : false;
+ }
+
+ private @Nullable T poll() {
+ if (this.elements.length == 0) {
+ return null;
+ }
+ // Attempt to poll an element while avoiding acquiring the lock for as long as possible
+ attemptWhileAvoidingLock: while (true) {
+ // Store field in local variable for non-volatile access
+ boolean[] elements = this.elements;
+ int indexToTry = this.potentialLowestPresentElementIndex;
+ if (!elements[indexToTry]) {
+ indexToTry = 0;
+ // On breaking, we found an index with a true value, let's acquire the lock and try
+ while (!elements[indexToTry]) {
+ indexToTry++;
+ if (indexToTry == elements.length) {
+ // We tried every index, stop attempting to poll an element while avoiding acquiring the lock
+ break attemptWhileAvoidingLock;
+ }
+ }
+ }
+ //noinspection StatementWithEmptyBody
+ while (!this.lock.tryAcquire());
+ try {
+ // Store field in local variable for non-volatile access
+ elements = this.elements;
+ if (elements[indexToTry]) {
+ elements[indexToTry] = false;
+ return this.getThreadByIndex(indexToTry);
+ }
+ } finally {
+ this.lock.release();
+ }
+ }
+ /*
+ Polling an element while avoiding acquiring the lock for as long as possible failed,
+ we will now quickly try again with the lock to be sure.
+ */
+ //noinspection StatementWithEmptyBody
+ while (!this.lock.tryAcquire());
+ try {
+ // Store fields in local variables for non-volatile access
+ boolean[] elements = this.elements;
+ int potentialLowestPresentElementIndex = this.potentialLowestPresentElementIndex;
+ if (elements[potentialLowestPresentElementIndex]) {
+ elements[potentialLowestPresentElementIndex] = false;
+ return this.getThreadByIndex(potentialLowestPresentElementIndex);
+ }
+ for (int index = 0; index < elements.length; index++) {
+ if (elements[index]) {
+ elements[index] = false;
+ return this.getThreadByIndex(index);
+ }
+ }
+ } finally {
+ this.lock.release();
+ }
+ // Polling an element failed
+ return null;
+ }
+
+ /**
+ * @return The index of the given thread, to be used as input to {@link #getThreadByIndex(int)} later.
+ */
+ protected abstract int getThreadIndex(T thread);
+
+ /**
+ * @return The thread with the given {@link #getThreadIndex}. This is always non-null
+ * as this method will never be called with an invalid index.
+ */
+ protected abstract @NotNull T getThreadByIndex(int index);
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/thread/wait/SignalReason.java b/src/main/java/org/galemc/gale/executor/thread/wait/SignalReason.java
new file mode 100644
index 0000000000000000000000000000000000000000..e6189d5b796e369566ed6322a41f57cf4e347981
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/thread/wait/SignalReason.java
@@ -0,0 +1,56 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.thread.wait;
+
+import org.galemc.gale.executor.annotation.thread.AnyThreadSafe;
+import org.galemc.gale.executor.annotation.YieldFree;
+import org.galemc.gale.executor.thread.SignallableThread;
+import org.galemc.gale.executor.thread.pooled.AbstractYieldingSignallableThreadPool;
+
+/**
+ * An interface to indicate the reason of a call to {@link SignallableThread#signal}.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+public interface SignalReason {
+
+ /**
+ * Signals another {@link SignallableThread} that is waiting for the same reason.
+ *
+ * @return Whether any thread was signalled.
+ */
+ boolean signalAnother();
+
+ @AnyThreadSafe
+ @YieldFree
+ static SignalReason createForThreadPoolNewTasks(AbstractYieldingSignallableThreadPool threadPool, boolean yielding) {
+ return new SignalReason() {
+
+ @Override
+ public boolean signalAnother() {
+ return threadPool.signalNewTasks(this, yielding);
+ }
+
+ };
+ }
+
+ @AnyThreadSafe
+ @YieldFree
+ static SignalReason createForWaitingThreadSet(WaitingThreadSet waitingThreads) {
+ return new SignalReason() {
+
+ @Override
+ public boolean signalAnother() {
+ return waitingThreads.pollAndSignal(this);
+ }
+
+ };
+ }
+
+ @AnyThreadSafe
+ @YieldFree
+ static SignalReason createNonRetrying() {
+ return () -> false;
+ }
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/thread/wait/WaitingBaseThreadSet.java b/src/main/java/org/galemc/gale/executor/thread/wait/WaitingBaseThreadSet.java
new file mode 100644
index 0000000000000000000000000000000000000000..d0cabea27a7cb1f18ed9ffe46409968cbbd8f891
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/thread/wait/WaitingBaseThreadSet.java
@@ -0,0 +1,121 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.thread.wait;
+
+import org.galemc.gale.concurrent.Mutex;
+import org.galemc.gale.executor.thread.BaseYieldingThread;
+import org.galemc.gale.executor.thread.pooled.AsyncThread;
+import org.galemc.gale.executor.thread.pooled.AsyncThreadPool;
+import org.galemc.gale.executor.thread.pooled.ServerThread;
+import org.galemc.gale.executor.thread.pooled.TickAssistThread;
+import org.galemc.gale.executor.thread.pooled.TickAssistThreadPool;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * A set of waiting {@link BaseYieldingThread}s that are an instance of either {@link ServerThread},
+ * {@link TickAssistThread} or {@link AsyncThread}.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+public final class WaitingBaseThreadSet implements WaitingThreadSet {
+
+ /**
+ * The {@link ServerThread}s in this collection,
+ * or null if no {@link ServerThread} was ever added to this collection.
+ */
+ private @Nullable WaitingServerThreadSet serverThreads;
+
+ /**
+ * The {@link TickAssistThread}s in this collection,
+ * or null if no {@link ServerThread} was ever added to this collection.
+ */
+ private @Nullable WaitingThreadSet tickAssistThreads;
+
+ /**
+ * The {@link AsyncThread}s in this collection,
+ * or null if no {@link ServerThread} was ever added to this collection.
+ */
+ private @Nullable WaitingThreadSet asyncThreads;
+
+ /**
+ * A simple lock for initializing the subsets in this collection.
+ */
+ private final Mutex initializeSubsetsLock = Mutex.create();
+
+ @Override
+ public void add(Thread thread) {
+ if (thread instanceof ServerThread) {
+ if (this.serverThreads == null) {
+ //noinspection StatementWithEmptyBody
+ while (!this.initializeSubsetsLock.tryAcquire());
+ try {
+ if (this.serverThreads == null) {
+ this.serverThreads = new WaitingServerThreadSet();
+ }
+ } finally {
+ this.initializeSubsetsLock.release();
+ }
+ }
+ this.serverThreads.add(thread);
+ } else if (thread instanceof TickAssistThread) {
+ if (this.tickAssistThreads == null) {
+ //noinspection StatementWithEmptyBody
+ while (!this.initializeSubsetsLock.tryAcquire());
+ try {
+ if (this.tickAssistThreads == null) {
+ this.tickAssistThreads = TickAssistThreadPool.instance.new WaitingMemberThreadSet();
+ }
+ } finally {
+ this.initializeSubsetsLock.release();
+ }
+ }
+ this.tickAssistThreads.add(thread);
+ } else if (thread instanceof AsyncThread) {
+ if (this.asyncThreads == null) {
+ //noinspection StatementWithEmptyBody
+ while (!this.initializeSubsetsLock.tryAcquire());
+ try {
+ if (this.asyncThreads == null) {
+ this.asyncThreads = AsyncThreadPool.instance.new WaitingMemberThreadSet();
+ }
+ } finally {
+ this.initializeSubsetsLock.release();
+ }
+ }
+ this.asyncThreads.add(thread);
+ }
+ throw new IllegalArgumentException("WaitingBaseThreadSet.add received a thread that is not a base thread");
+ }
+
+ @Override
+ public void remove(Thread thread) {
+ if (thread instanceof ServerThread) {
+ if (this.serverThreads != null) {
+ this.serverThreads.remove(thread);
+ }
+ } else if (thread instanceof TickAssistThread) {
+ if (this.tickAssistThreads != null) {
+ this.tickAssistThreads.add(thread);
+ }
+ } else if (thread instanceof AsyncThread) {
+ if (this.asyncThreads != null) {
+ this.asyncThreads.remove(thread);
+ }
+ }
+ throw new IllegalArgumentException("WaitingBaseThreadSet.remove received a thread that is not a base thread");
+ }
+
+ @SuppressWarnings("RedundantIfStatement")
+ @Override
+ public boolean pollAndSignal(SignalReason reason) {
+ if (this.serverThreads != null && this.serverThreads.pollAndSignal(reason)) {
+ return true;
+ } else if (this.tickAssistThreads != null && this.tickAssistThreads.pollAndSignal(reason)) {
+ return true;
+ } else if (this.asyncThreads != null && this.asyncThreads.pollAndSignal(reason)) {
+ return true;
+ }
+ return false;
+ }
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/thread/wait/WaitingServerThreadSet.java b/src/main/java/org/galemc/gale/executor/thread/wait/WaitingServerThreadSet.java
new file mode 100644
index 0000000000000000000000000000000000000000..2e631d1c3c1cbbad9d22a187077e148b9f60071f
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/thread/wait/WaitingServerThreadSet.java
@@ -0,0 +1,37 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.thread.wait;
+
+import org.galemc.gale.executor.thread.pooled.ServerThread;
+
+/**
+ * A {@link WaitingThreadSet} that simply remembers whether a {@link ServerThread} is waiting for tasks.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+public class WaitingServerThreadSet implements WaitingThreadSet {
+
+ public volatile boolean waiting;
+
+ @Override
+ public void add(Thread thread) {
+ waiting = true;
+ }
+
+ @Override
+ public void remove(Thread thread) {
+ waiting = false;
+ }
+
+ @Override
+ public boolean pollAndSignal(SignalReason reason) {
+ if (waiting) {
+ //noinspection RedundantIfStatement
+ if (ServerThread.getInstance().signal(reason)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/src/main/java/org/galemc/gale/executor/thread/wait/WaitingThreadSet.java b/src/main/java/org/galemc/gale/executor/thread/wait/WaitingThreadSet.java
new file mode 100644
index 0000000000000000000000000000000000000000..a22f4815cbf6411ac32a070d1769c2389c8140b4
--- /dev/null
+++ b/src/main/java/org/galemc/gale/executor/thread/wait/WaitingThreadSet.java
@@ -0,0 +1,52 @@
+// Gale - base thread pools
+
+package org.galemc.gale.executor.thread.wait;
+
+import org.galemc.gale.executor.annotation.thread.AnyThreadSafe;
+import org.galemc.gale.executor.annotation.YieldFree;
+import org.galemc.gale.executor.lock.YieldingLock;
+import org.galemc.gale.executor.thread.SignallableThread;
+
+/**
+ * An interface for a set of waiting threads.
+ * This is used to collect the threads that are all waiting for new work,
+ * e.g. for new tasks to be added to a queue or for a {@link YieldingLock} to be released.
+ *
+ * @author Martijn Muijsers under AGPL-3.0
+ */
+@AnyThreadSafe
+@YieldFree
+public interface WaitingThreadSet {
+
+ /**
+ * Adds a waiting thread.
+ * <br>
+ * This method must only be called from the given thread itself.
+ */
+ void add(Thread thread);
+
+ /**
+ * Removes a waiting thread.
+ * <br>
+ * This method must only be called from the given thread itself.
+ */
+ void remove(Thread thread);
+
+ /**
+ * Attempts to signal one waiting thread.
+ * <br>
+ * Calling this method twice from different threads concurrently
+ * will never lead to signalling the same waiting thread:
+ * every thread is only signalled by this set at most one time per continuous period of time that it is present.
+ * <br>
+ * Calling this method may fail to signal a newly added thread that is added after this method is called,
+ * but before it returns, and may also fail to signal a just removed thread
+ * that was removed after this method was called, but before it returned.
+ *
+ * @return Whether a thread was signalled (this return value is always accurate).
+ *
+ * @see SignallableThread#signal
+ */
+ boolean pollAndSignal(SignalReason reason);
+
+}
diff --git a/src/main/java/org/spigotmc/SpigotCommand.java b/src/main/java/org/spigotmc/SpigotCommand.java
index 3112a8695639c402e9d18710acbc11cff5611e9c..505181f041fc45a8812bf0b5199950022d3b3001 100644
--- a/src/main/java/org/spigotmc/SpigotCommand.java
+++ b/src/main/java/org/spigotmc/SpigotCommand.java
@@ -31,7 +31,7 @@ public class SpigotCommand extends Command {
MinecraftServer console = MinecraftServer.getServer();
org.spigotmc.SpigotConfig.init((File) console.options.valueOf("spigot-settings"));
- for (ServerLevel world : console.getAllLevels()) {
+ for (ServerLevel world : console.getAllLevelsArray()) { // Gale - base thread pools - optimize server levels
world.spigotConfig.init();
}
console.server.reloadCount++;
diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java
index 832f1ee4fb11c981bd109510eb908d7c7ef91bd4..8cd1b04dafc16dc96a6ad58fef7930b351c8f147 100644
--- a/src/main/java/org/spigotmc/WatchdogThread.java
+++ b/src/main/java/org/spigotmc/WatchdogThread.java
@@ -1,6 +1,5 @@
package org.spigotmc;
-import java.awt.print.Paper;
import java.lang.management.ManagementFactory;
import java.lang.management.MonitorInfo;
import java.lang.management.ThreadInfo;
@@ -8,12 +7,13 @@ import java.util.logging.Level;
import java.util.logging.Logger;
import net.minecraft.server.MinecraftServer;
import org.bukkit.Bukkit;
+import org.galemc.gale.executor.thread.pooled.ServerThread;
-public final class WatchdogThread extends io.papermc.paper.util.TickThread // Paper - rewrite chunk system
+public final class WatchdogThread extends ServerThread // Paper - rewrite chunk system // Gale - base thread pools
{
public static final boolean DISABLE_WATCHDOG = Boolean.getBoolean("disable.watchdog"); // Paper
- private static WatchdogThread instance;
+ public static WatchdogThread instance; // Gale - base thread pools - private -> public
private long timeoutTime;
private boolean restart;
private final long earlyWarningEvery; // Paper - Timeout time for just printing a dump but not restarting
@@ -206,7 +206,7 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa
log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Gale!):" ); // Paper // Gale - branding changes
io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(isLongTimeout); // Paper // Paper - rewrite chunk system
this.dumpTickingInfo(); // Paper - log detailed tick information
- WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log );
+ WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.serverThread.getId(), Integer.MAX_VALUE ), log ); // Gale - base thread pools
log.log( Level.SEVERE, "------------------------------" );
//
// Paper start - Only print full dump on long timeouts