From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Martijn Muijsers Date: Fri, 2 Dec 2022 11:43:51 +0100 Subject: [PATCH] Base thread pool License: AGPL-3.0 (https://www.gnu.org/licenses/agpl-3.0.html) Gale - https://galemc.org diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java index b71404be2c82f7db35272b367af861e94d6c73d3..0b4ae235398eda804d6facd4db74c721e9d76b57 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 pool + protected final ArrayDeque[] queues = new ArrayDeque[Priority.TOTAL_SCHEDULABLE_PRIORITIES]; { for (int i = 0; i < Priority.TOTAL_SCHEDULABLE_PRIORITIES; ++i) { this.queues[i] = new ArrayDeque<>(); @@ -20,6 +25,16 @@ public class PrioritisedThreadedTaskQueue implements PrioritisedExecutor { protected long taskIdGenerator = 0; + public PrioritisedThreadedTaskQueue() { + this(false); + } + + // Gale start - base thread pool + public PrioritisedThreadedTaskQueue(boolean influenceMayHaveDelayedTasks) { + this.influenceMayHaveDelayedTasks = influenceMayHaveDelayedTasks; + } + // Gale end - base thread pool + @Override public PrioritisedExecutor.PrioritisedTask queueRunnable(final Runnable task, final PrioritisedExecutor.Priority priority) throws IllegalStateException, IllegalArgumentException { if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { @@ -145,6 +160,12 @@ public class PrioritisedThreadedTaskQueue implements PrioritisedExecutor { } protected final long getAndAddTotalScheduledTasksVolatile(final long value) { + // Gale start - base thread pool + if (this.influenceMayHaveDelayedTasks) { + MinecraftServer.nextTimeAssumeWeMayHaveDelayedTasks = true; + BaseTaskQueues.allLevelsScheduledTickThreadChunk.newTaskWasAdded(); + } + // Gale end - base thread pool return this.totalScheduledTasks.getAndAdd(value); } @@ -158,6 +179,12 @@ public class PrioritisedThreadedTaskQueue implements PrioritisedExecutor { return this.totalCompletedTasks.getAndAdd(value); } + // Gale start - base thread pool + public final boolean hasScheduledUncompletedTasksVolatile() { + return this.totalScheduledTasks.get() > this.totalCompletedTasks.get(); + } + // Gale end - base thread pool + protected static final class PrioritisedTask implements PrioritisedExecutor.PrioritisedTask { protected final PrioritisedThreadedTaskQueue queue; protected long id; diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java index 4f3670b2bdb8b1b252e9f074a6af56a018a8c465..aa7467c0ce302c27d77f0af032b81c4f8ef9408d 100644 --- a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java +++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java @@ -22,6 +22,7 @@ import net.minecraft.world.level.block.EntityBlock; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.chunk.*; import org.bukkit.Bukkit; +import org.galemc.gale.executor.queue.ScheduledServerThreadTaskQueues; import java.util.*; import java.util.concurrent.Executor; @@ -181,7 +182,7 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo if (!Bukkit.isPrimaryThread()) { // Plugins? - MinecraftServer.getServer().scheduleOnMain(() -> modifyBlocks(chunkPacket, chunkPacketInfo)); + ScheduledServerThreadTaskQueues.add(() -> modifyBlocks(chunkPacket, chunkPacketInfo), ScheduledServerThreadTaskQueues.ANTI_XRAY_MODIFY_BLOCKS_TASK_MAX_DELAY); // Gale - base thread pool return; } diff --git a/src/main/java/com/mojang/logging/LogUtils.java b/src/main/java/com/mojang/logging/LogUtils.java index 49019b4a9bc4e634d54a9b0acaf9229a5c896f85..6aae3b36bfe3ffc630cd7af250633de3444095e8 100644 --- a/src/main/java/com/mojang/logging/LogUtils.java +++ b/src/main/java/com/mojang/logging/LogUtils.java @@ -6,6 +6,7 @@ import org.apache.logging.log4j.core.LifeCycle; import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.LoggerConfig; import org.apache.logging.log4j.spi.LoggerContext; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.Marker; @@ -66,4 +67,329 @@ public class LogUtils { return LoggerFactory.getLogger(STACK_WALKER.getCallerClass().getSimpleName()); } // Paper end + + // Gale start - base thread pool - thread loggers + public static @NotNull Logger prefixLogger(@NotNull Logger logger, @NotNull Supplier<@NotNull String> prefixSupplier) { + return new org.slf4j.Logger() { + @Override + public String getName() { + return logger.getName(); + } + + @Override + public boolean isTraceEnabled() { + return logger.isTraceEnabled(); + } + + @Override + public void trace(String msg) { + logger.trace(prefixSupplier.get() + msg); + } + + @Override + public void trace(String format, Object arg) { + logger.trace(prefixSupplier.get() + format, arg); + } + + @Override + public void trace(String format, Object arg1, Object arg2) { + logger.trace(prefixSupplier.get() + format, arg1, arg2); + } + + @Override + public void trace(String format, Object... arguments) { + logger.trace(prefixSupplier.get() + format, arguments); + } + + @Override + public void trace(String msg, Throwable t) { + logger.trace(prefixSupplier.get() + msg, t); + } + + @Override + public boolean isTraceEnabled(Marker marker) { + return logger.isTraceEnabled(marker); + } + + @Override + public void trace(Marker marker, String msg) { + logger.trace(marker, prefixSupplier.get() + msg); + } + + @Override + public void trace(Marker marker, String format, Object arg) { + logger.trace(marker, prefixSupplier.get() + format, arg); + } + + @Override + public void trace(Marker marker, String format, Object arg1, Object arg2) { + logger.trace(marker, prefixSupplier.get() + format, arg1, arg2); + } + + @Override + public void trace(Marker marker, String format, Object... argArray) { + logger.trace(marker, prefixSupplier.get() + format, argArray); + } + + @Override + public void trace(Marker marker, String msg, Throwable t) { + logger.trace(marker, prefixSupplier.get() + msg, t); + } + + @Override + public boolean isDebugEnabled() { + return logger.isDebugEnabled(); + } + + @Override + public void debug(String msg) { + logger.debug(prefixSupplier.get() + msg); + } + + @Override + public void debug(String format, Object arg) { + logger.debug(prefixSupplier.get() + format, arg); + } + + @Override + public void debug(String format, Object arg1, Object arg2) { + logger.debug(prefixSupplier.get() + format, arg1, arg2); + } + + @Override + public void debug(String format, Object... arguments) { + logger.debug(prefixSupplier.get() + format, arguments); + } + + @Override + public void debug(String msg, Throwable t) { + logger.debug(prefixSupplier.get() + msg, t); + } + + @Override + public boolean isDebugEnabled(Marker marker) { + return logger.isDebugEnabled(marker); + } + + @Override + public void debug(Marker marker, String msg) { + logger.debug(marker, prefixSupplier.get() + msg); + } + + @Override + public void debug(Marker marker, String format, Object arg) { + logger.debug(marker, prefixSupplier.get() + format, arg); + } + + @Override + public void debug(Marker marker, String format, Object arg1, Object arg2) { + logger.debug(marker, prefixSupplier.get() + format, arg1, arg2); + } + + @Override + public void debug(Marker marker, String format, Object... arguments) { + logger.debug(marker, prefixSupplier.get() + format, arguments); + } + + @Override + public void debug(Marker marker, String msg, Throwable t) { + logger.debug(marker, prefixSupplier.get() + msg, t); + } + + @Override + public boolean isInfoEnabled() { + return logger.isInfoEnabled(); + } + + @Override + public void info(String msg) { + logger.info(prefixSupplier.get() + msg); + } + + @Override + public void info(String format, Object arg) { + logger.info(prefixSupplier.get() + format, arg); + } + + @Override + public void info(String format, Object arg1, Object arg2) { + logger.info(prefixSupplier.get() + format, arg1, arg2); + } + + @Override + public void info(String format, Object... arguments) { + logger.info(prefixSupplier.get() + format, arguments); + } + + @Override + public void info(String msg, Throwable t) { + logger.info(prefixSupplier.get() + msg, t); + } + + @Override + public boolean isInfoEnabled(Marker marker) { + return logger.isInfoEnabled(marker); + } + + @Override + public void info(Marker marker, String msg) { + logger.info(marker, prefixSupplier.get() + msg); + } + + @Override + public void info(Marker marker, String format, Object arg) { + logger.info(marker, prefixSupplier.get() + format, arg); + } + + @Override + public void info(Marker marker, String format, Object arg1, Object arg2) { + logger.info(marker, prefixSupplier.get() + format, arg1, arg2); + } + + @Override + public void info(Marker marker, String format, Object... arguments) { + logger.info(marker, prefixSupplier.get() + format, arguments); + } + + @Override + public void info(Marker marker, String msg, Throwable t) { + logger.info(marker, prefixSupplier.get() + msg, t); + } + + @Override + public boolean isWarnEnabled() { + return logger.isWarnEnabled(); + } + + @Override + public void warn(String msg) { + logger.warn(prefixSupplier.get() + msg); + } + + @Override + public void warn(String format, Object arg) { + logger.warn(prefixSupplier.get() + format, arg); + } + + @Override + public void warn(String format, Object arg1, Object arg2) { + logger.warn(prefixSupplier.get() + format, arg1, arg2); + } + + @Override + public void warn(String format, Object... arguments) { + logger.warn(prefixSupplier.get() + format, arguments); + } + + @Override + public void warn(String msg, Throwable t) { + logger.warn(prefixSupplier.get() + msg, t); + } + + @Override + public boolean isWarnEnabled(Marker marker) { + return logger.isWarnEnabled(marker); + } + + @Override + public void warn(Marker marker, String msg) { + logger.warn(marker, prefixSupplier.get() + msg); + } + + @Override + public void warn(Marker marker, String format, Object arg) { + logger.warn(marker, prefixSupplier.get() + format, arg); + } + + @Override + public void warn(Marker marker, String format, Object arg1, Object arg2) { + logger.warn(marker, prefixSupplier.get() + format, arg1, arg2); + } + + @Override + public void warn(Marker marker, String format, Object... arguments) { + logger.warn(marker, prefixSupplier.get() + format, arguments); + } + + @Override + public void warn(Marker marker, String msg, Throwable t) { + logger.warn(marker, prefixSupplier.get() + msg, t); + } + + @Override + public boolean isErrorEnabled() { + return logger.isErrorEnabled(); + } + + @Override + public void error(String msg) { + logger.error(prefixSupplier.get() + msg); + } + + @Override + public void error(String format, Object arg) { + logger.error(prefixSupplier.get() + format, arg); + } + + @Override + public void error(String format, Object arg1, Object arg2) { + logger.error(prefixSupplier.get() + format, arg1, arg2); + } + + @Override + public void error(String format, Object... arguments) { + logger.error(prefixSupplier.get() + format, arguments); + } + + @Override + public void error(String msg, Throwable t) { + logger.error(prefixSupplier.get() + msg, t); + } + + @Override + public boolean isErrorEnabled(Marker marker) { + return logger.isErrorEnabled(marker); + } + + @Override + public void error(Marker marker, String msg) { + logger.error(marker, prefixSupplier.get() + msg); + } + + @Override + public void error(Marker marker, String format, Object arg) { + logger.error(marker, prefixSupplier.get() + format, arg); + } + + @Override + public void error(Marker marker, String format, Object arg1, Object arg2) { + logger.error(marker, prefixSupplier.get() + format, arg1, arg2); + } + + @Override + public void error(Marker marker, String format, Object... arguments) { + logger.error(marker, prefixSupplier.get() + format, arguments); + } + + @Override + public void error(Marker marker, String msg, Throwable t) { + logger.error(marker, prefixSupplier.get() + msg, t); + } + + }; + } + + public static @NotNull Logger prefixLoggerWithThread(@NotNull Logger logger) { + return prefixLogger(logger, () -> "[" + Thread.currentThread().getName() + "] "); + } + + public static @NotNull Logger getLoggerPrefixedWithThread() { + return prefixLoggerWithThread(LoggerFactory.getLogger(STACK_WALKER.getCallerClass())); + } + + public static @NotNull Logger getClassLoggerPrefixedWithThread() { + return prefixLoggerWithThread(LoggerFactory.getLogger(STACK_WALKER.getCallerClass().getSimpleName())); + } + // Gale end - base thread pool - thread loggers + } diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java index 84cc9397237fa0c17aa1012dfb5683c90eb6d3b8..f5c15d40094c2ddc6220b0595597d12103fcf425 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 - base thread pool - private -> public, count delayed tasks final ReentrantLock schedulingLock = new ReentrantLock(); public final ChunkHolderManager chunkHolderManager; diff --git a/src/main/java/io/papermc/paper/configuration/Configurations.java b/src/main/java/io/papermc/paper/configuration/Configurations.java index cf6d50218769e3fecd12dbde70a03b5042feddf4..9d8ee965f7dcd0f416b7aa8368e34b911edef6b0 100644 --- a/src/main/java/io/papermc/paper/configuration/Configurations.java +++ b/src/main/java/io/papermc/paper/configuration/Configurations.java @@ -322,7 +322,7 @@ public abstract class Configurations { YamlConfiguration global = YamlConfiguration.loadConfiguration(this.globalFolder.resolve(this.globalConfigFileName).toFile()); ConfigurationSection worlds = global.createSection(legacyWorldsSectionKey); worlds.set(legacyWorldDefaultsSectionKey, YamlConfiguration.loadConfiguration(this.globalFolder.resolve(this.defaultWorldConfigFileName).toFile())); - for (ServerLevel level : server.getAllLevels()) { + for (ServerLevel level : server.getAllLevelsArray()) { // Gale - base thread pool - optimize server levels worlds.set(level.getWorld().getName(), YamlConfiguration.loadConfiguration(getWorldConfigFile(level).toFile())); } return global; diff --git a/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java b/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java index a82be9c7226348b6c8ed5edfa8dd8262b4f49f07..47a3580caef45ffe71446c247d4e06e332b2fda2 100644 --- a/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java +++ b/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java @@ -289,7 +289,7 @@ public class PaperConfigurations extends Configurations(), - new ThreadFactoryBuilder() - .setNameFormat("Paper Async Task Handler Thread - %1$d") - .setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(MinecraftServer.LOGGER)) - .build() - ); - public static final ThreadPoolExecutor cleanerExecutor = new ThreadPoolExecutor( - 1, 1, 0L, TimeUnit.SECONDS, - new LinkedBlockingQueue<>(), - new ThreadFactoryBuilder() - .setNameFormat("Paper Object Cleaner") - .setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(MinecraftServer.LOGGER)) - .build() - ); + public static final Executor asyncExecutor = BaseTaskQueues.scheduledAsync.yieldingExecutor; // Gale - base thread pool - remove Paper async executor + public static final Executor cleanerExecutor = BaseTaskQueues.cleaner.executor; // Gale - base thread pool - remove Paper cleaner executor public static final long INVALID_CHUNK_KEY = getCoordinateKey(Integer.MAX_VALUE, Integer.MAX_VALUE); diff --git a/src/main/java/io/papermc/paper/util/TickThread.java b/src/main/java/io/papermc/paper/util/TickThread.java index fc57850b80303fcade89ca95794f63910404a407..04c678712f154c2da33e1e38c8583c40f385efed 100644 --- a/src/main/java/io/papermc/paper/util/TickThread.java +++ b/src/main/java/io/papermc/paper/util/TickThread.java @@ -3,10 +3,11 @@ 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.thread.BaseThread; + import java.util.concurrent.atomic.AtomicInteger; -public class TickThread extends Thread { +public abstract class TickThread extends BaseThread { // Gale - base thread pool public static final boolean STRICT_THREAD_CHECKS = Boolean.getBoolean("paper.strict-thread-checks"); @@ -65,7 +66,7 @@ public class TickThread extends Thread { } private TickThread(final Runnable run, final String name, final int id) { - super(run, name); + super(run, name, 0, 1); // Gale - base thread pool 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..a376259202b4a16c67db4d3ef071e0b395aca524 100644 --- a/src/main/java/io/papermc/paper/world/ThreadedWorldUpgrader.java +++ b/src/main/java/io/papermc/paper/world/ThreadedWorldUpgrader.java @@ -7,25 +7,21 @@ import net.minecraft.nbt.CompoundTag; import net.minecraft.resources.ResourceKey; import net.minecraft.util.worldupdate.WorldUpgrader; import net.minecraft.world.level.ChunkPos; -import net.minecraft.world.level.Level; import net.minecraft.world.level.chunk.ChunkGenerator; import net.minecraft.world.level.chunk.storage.ChunkStorage; import net.minecraft.world.level.chunk.storage.RegionFileStorage; -import net.minecraft.world.level.dimension.DimensionType; import net.minecraft.world.level.dimension.LevelStem; -import net.minecraft.world.level.levelgen.WorldGenSettings; import net.minecraft.world.level.storage.DimensionDataStorage; import net.minecraft.world.level.storage.LevelStorageSource; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.galemc.gale.executor.queue.BaseTaskQueues; + import java.io.File; import java.io.IOException; import java.text.DecimalFormat; import java.util.Optional; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Supplier; @@ -46,6 +42,10 @@ public class ThreadedWorldUpgrader { this.dimensionType = dimensionType; this.worldName = worldName; this.worldDir = worldDir; + // Gale start - base thread pool - remove world upgrade executors + this.threadPool = BaseTaskQueues.scheduledAsync.yieldingExecutor; + /* + // Gale end - base thread pool - remove world upgrade executors this.threadPool = Executors.newFixedThreadPool(Math.max(1, threads), new ThreadFactory() { private final AtomicInteger threadCounter = new AtomicInteger(); @@ -61,6 +61,7 @@ public class ThreadedWorldUpgrader { return ret; } }); + */ // Gale - base thread pool - remove world upgrade executors this.dataFixer = dataFixer; this.generatorKey = generatorKey; this.removeCaches = removeCaches; diff --git a/src/main/java/net/minecraft/Util.java b/src/main/java/net/minecraft/Util.java index 5ef58831a857fd8aa4ac30147762dc17d773a53e..6b109d92dec227d4a91a455caf596beb112a8351 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 pool - remove background executor + private static final ExecutorService BOOTSTRAP_EXECUTOR = BACKGROUND_EXECUTOR; // Gale - Patina - remove bootstrap executor // Paper start - don't submit BLOCKING PROFILE LOOKUPS to the world gen thread public static final ExecutorService PROFILE_EXECUTOR = Executors.newFixedThreadPool(2, new java.util.concurrent.ThreadFactory() { @@ -219,7 +214,6 @@ public class Util { } public static void shutdownExecutors() { - shutdownExecutor(BACKGROUND_EXECUTOR); shutdownExecutor(IO_POOL); } diff --git a/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java b/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java index f25b9330e068c7d9e12cb57a7761cfef9ebaf7bc..64d957ba23d306327a26605e1e42f32fa741e2cb 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 pool - optimize server levels this.addEntities(list, worldserver1, vec3d, predicate); } diff --git a/src/main/java/net/minecraft/network/protocol/PacketUtils.java b/src/main/java/net/minecraft/network/protocol/PacketUtils.java index 27d4aa45e585842c04491839826d405d6f447f0e..0a54e07db5a55b6170650c070bb19e07b32410a7 100644 --- a/src/main/java/net/minecraft/network/protocol/PacketUtils.java +++ b/src/main/java/net/minecraft/network/protocol/PacketUtils.java @@ -2,6 +2,7 @@ package net.minecraft.network.protocol; import com.mojang.logging.LogUtils; import net.minecraft.network.PacketListener; +import org.galemc.gale.executor.AbstractBlockableEventLoop; import org.slf4j.Logger; // CraftBukkit start @@ -9,7 +10,6 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.server.RunningOnDifferentThreadException; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.network.ServerGamePacketListenerImpl; -import net.minecraft.util.thread.BlockableEventLoop; public class PacketUtils { @@ -36,10 +36,10 @@ public class PacketUtils { public PacketUtils() {} public static void ensureRunningOnSameThread(Packet packet, T listener, ServerLevel world) throws RunningOnDifferentThreadException { - PacketUtils.ensureRunningOnSameThread(packet, listener, (BlockableEventLoop) world.getServer()); + PacketUtils.ensureRunningOnSameThread(packet, listener, world.getServer()); // Gale - base thread pool } - public static void ensureRunningOnSameThread(Packet packet, T listener, BlockableEventLoop engine) throws RunningOnDifferentThreadException { + public static void ensureRunningOnSameThread(Packet packet, T listener, AbstractBlockableEventLoop engine) throws RunningOnDifferentThreadException { // Gale - base thread pool if (!engine.isSameThread()) { engine.execute(() -> { // Paper - Fix preemptive player kick on a server shutdown. packetProcessing.push(listener); // Paper - detailed watchdog information diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java index 0c4c62674b4c7e8e3921c7eb3ef726759ac75075..40f20806cc06106b4aa8e708467dcea94d23c83e 100644 --- a/src/main/java/net/minecraft/server/Main.java +++ b/src/main/java/net/minecraft/server/Main.java @@ -1,27 +1,22 @@ package net.minecraft.server; -import com.mojang.authlib.GameProfile; -import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService; import com.mojang.datafixers.DataFixer; import com.mojang.datafixers.util.Pair; import com.mojang.logging.LogUtils; import com.mojang.serialization.DynamicOps; -import com.mojang.serialization.Lifecycle; + import java.awt.GraphicsEnvironment; import java.io.File; import java.net.Proxy; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Optional; -import java.util.UUID; import java.util.function.BooleanSupplier; + +import com.mojang.serialization.Lifecycle; import io.papermc.paper.world.ThreadedWorldUpgrader; -import joptsimple.NonOptionArgumentSpec; -import joptsimple.OptionParser; import joptsimple.OptionSet; -import joptsimple.OptionSpec; import net.minecraft.CrashReport; -import net.minecraft.DefaultUncaughtExceptionHandler; import net.minecraft.SharedConstants; import net.minecraft.Util; import net.minecraft.commands.Commands; @@ -57,6 +52,7 @@ import net.minecraft.world.level.storage.LevelStorageSource; import net.minecraft.world.level.storage.LevelSummary; import net.minecraft.world.level.storage.PrimaryLevelData; import net.minecraft.world.level.storage.WorldData; +import org.galemc.gale.executor.queue.BaseTaskQueueTier; import org.slf4j.Logger; // CraftBukkit start @@ -64,7 +60,7 @@ import com.google.common.base.Charsets; import com.mojang.bridge.game.PackType; import java.io.InputStreamReader; import java.util.concurrent.atomic.AtomicReference; -import net.minecraft.SharedConstants; + import org.bukkit.configuration.file.YamlConfiguration; // CraftBukkit end @@ -228,6 +224,12 @@ public class Main { WorldStem worldstem; + // Gale start - base thread pool + // Initialize the task tiers and queues by calling an arbitrary method on the last tier and queue + //noinspection ResultOfMethodCallIgnored + BaseTaskQueueTier.ASYNC.ordinal(); + // Gale end - base thread pool + try { WorldLoader.InitConfig worldloader_c = Main.loadOrCreateConfig(dedicatedserversettings.getProperties(), convertable_conversionsession, flag, resourcepackrepository); diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java index eb951c9fda85d9620d3038a3db22d578db45e878..77b4d5050a5e708522cc07e819db2b3619c46518 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -40,10 +40,8 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; -import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BooleanSupplier; -import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -108,19 +106,8 @@ import net.minecraft.util.ProgressListener; import net.minecraft.util.RandomSource; import net.minecraft.util.SignatureValidator; import net.minecraft.util.datafix.DataFixers; -import net.minecraft.util.profiling.EmptyProfileResults; -import net.minecraft.util.profiling.ProfileResults; -import net.minecraft.util.profiling.ProfilerFiller; -import net.minecraft.util.profiling.ResultField; -import net.minecraft.util.profiling.SingleTickProfiler; import net.minecraft.util.profiling.jfr.JvmProfiler; import net.minecraft.util.profiling.jfr.callback.ProfiledDuration; -import net.minecraft.util.profiling.metrics.profiling.ActiveMetricsRecorder; -import net.minecraft.util.profiling.metrics.profiling.InactiveMetricsRecorder; -import net.minecraft.util.profiling.metrics.profiling.MetricsRecorder; -import net.minecraft.util.profiling.metrics.profiling.ServerMetricsSamplersProvider; -import net.minecraft.util.profiling.metrics.storage.MetricsPersister; -import net.minecraft.util.thread.ReentrantBlockableEventLoop; import net.minecraft.world.Difficulty; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.ai.village.VillageSiege; @@ -161,7 +148,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.SignalReason; +import org.galemc.gale.executor.thread.pool.BaseThreadActivation; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; // CraftBukkit start @@ -181,23 +176,26 @@ import net.minecraft.world.level.levelgen.PatrolSpawner; import net.minecraft.world.level.levelgen.PhantomSpawner; import net.minecraft.world.level.levelgen.WorldDimensions; import net.minecraft.world.level.levelgen.presets.WorldPresets; -import org.bukkit.Bukkit; -import org.bukkit.craftbukkit.CraftServer; -import org.bukkit.craftbukkit.Main; -import org.bukkit.craftbukkit.util.CraftChatMessage; -import org.bukkit.craftbukkit.util.LazyPlayerSet; -import org.bukkit.event.player.AsyncPlayerChatPreviewEvent; import org.bukkit.event.server.ServerLoadEvent; // CraftBukkit end import co.aikar.timings.MinecraftTimings; // Paper -public abstract class MinecraftServer extends ReentrantBlockableEventLoop implements CommandSource, AutoCloseable { +public abstract class MinecraftServer extends MinecraftServerBlockableEventLoop implements CommandSource, AutoCloseable { // Gale - base thread pool public static final int SERVER_THREAD_PRIORITY = Integer.getInteger("gale.thread.priority.server", 8); // Gale - server thread priority environment variable - private static MinecraftServer SERVER; // Paper + // Gale start - base thread pool + public static MinecraftServer SERVER; // Paper // Gale - base thread pool - private -> public + + /** + * Whether {@link #SERVER} has been set. + */ + public static boolean isConstructed; + + // Gale end - base thread pool public static final Logger LOGGER = LogUtils.getLogger(); + public static final Optional THREAD_DEBUG_LOGGER = Boolean.FALSE ? Optional.of(LogUtils.prefixLoggerWithThread(LogUtils.prefixLogger(LogUtils.getLogger(), () -> "TEMP DEBUG - "))) : Optional.empty(); // Gale - base thread pool - temporary debug logger public static final String VANILLA_BRAND = "vanilla"; private static final float AVERAGE_TICK_TIME_SMOOTHING = 0.8F; private static final int TICK_STATS_SPAN = 100; @@ -226,6 +224,10 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop registries; private Map, ServerLevel> levels; + // Gale start - base thread pool - optimize server levels + private @NotNull ServerLevel @NotNull [] levelArray = ArrayConstants.emptyServerLevelArray; + private @Nullable ServerLevel overworld; + // Gale end - base thread pool - optimize server levels private PlayerList playerList; private volatile boolean running; private volatile boolean isRestarting = false; // Paper - flag to signify we're attempting to restart @@ -255,10 +257,114 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop static, final -> non-final (but still effectively final) + // Gale start - base thread pool - make fields volatile + private volatile long nextTickTime; + private volatile long delayedTasksMaxNextTickTime; + // Gale end - base thread pool - make fields volatile + + // Gale start - base thread pool + + public static volatile long nextTickStartNanoTime; + public static volatile long delayedTasksMaxNextTickNanoTime; + + /** + * Sets {@link #nextTickTime}, and sets {@link #nextTickStartNanoTime} accordingly. + */ + private void setNextTickTime(long nextTickTime) { + this.nextTickTime = nextTickTime; + /* + Add 10000 nanoseconds, to make sure the currentTime() >= nextTickTime check will be true after this moment + regardless of the nanosecond granularity of the Condition#await function, which is probably somewhere around + 26 nanoseconds. + */ + nextTickStartNanoTime = 1_000_000L * this.nextTickTime + 10_000L; + } + + /** + * Sets {@link #delayedTasksMaxNextTickTime}, and sets {@link #delayedTasksMaxNextTickNanoTime} accordingly. + * + * @see #setNextTickTime + */ + private void setDelayedTasksMaxNextTickTime(long delayedTasksMaxNextTickTime) { + this.delayedTasksMaxNextTickTime = delayedTasksMaxNextTickTime; + delayedTasksMaxNextTickNanoTime = 1_000_000L * this.delayedTasksMaxNextTickTime + 10_000L; + } + + /** + * Whether to skip the next call to {@link #mayHaveDelayedTasks()} and simply return true. + * This is typically set to true when a new task is added to a queue with tasks that count as potentially + * delayed tasks, or when an element from such a queue is successfully polled (even though it may afterwards be + * empty, it seems better to simply poll again next time, rather than perform the full {@link #mayHaveDelayedTasks()} + * check that loops over all queues). + */ + public static volatile boolean nextTimeAssumeWeMayHaveDelayedTasks; + + /** + * Whether the value of {@link #lastComputedMayHaveDelayedTasks} should be assumed to be correct. + */ + public static volatile boolean mayHaveDelayedTasksIsCurrentlyComputed; + + /** + * The cached last computed correct (except for potential race condition mistakes in the computation) + * value of {@link #mayHaveDelayedTasks()}. + */ + public static volatile boolean lastComputedMayHaveDelayedTasks; + + /** + * Whether the server is currently in spare time after a tick. + * This is set to true by the {@link #serverThread} when entering the spare time phase, + * either at the end of a tick, or at the start of one (if it occurred too early), and set to false after + * the corresponding {@link #managedBlock} call. + */ + public static volatile boolean isInSpareTime = false; + + /** + * Whether the server is currently waiting for the next tick, which is one of the cases where + * {@link #isInSpareTime} is true. Specifically, the other case where {@link #isInSpareTime} is true is + * while {@link #isOversleep} is true. + */ + public static volatile boolean isWaitingUntilNextTick = false; + + /** + * A potentially out-of-date value indicating whether {@link #isInSpareTime} is true + * and {@link #haveTime()} is false and {@link #blockingCount} is 0. + * This should be updated just in time before it is potentially needed. + */ + public static volatile boolean isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking = false; + + /** + * The stop condition provided to the current call of {@link #managedBlock}, or null if no {@link #managedBlock} + * call is ongoing. + */ + public static volatile @Nullable BooleanSupplier currentManagedBlockStopCondition; + + /** + * Whether the {@link #currentManagedBlockStopCondition} has become true + * during the last {@link #managedBlock} call. + */ + public static volatile boolean currentManagedBlockStopConditionHasBecomeTrue = false; + + public static void signalServerThreadIfCurrentManagedBlockStopConditionBecameTrue() { + if (currentManagedBlockStopConditionHasBecomeTrue) { + // We already signalled the thread + return; + } + var managedBlockStopCondition = currentManagedBlockStopCondition; + if (managedBlockStopCondition == null) { + // There is no ongoing managedBlock cal + return; + } + if (!managedBlockStopCondition.getAsBoolean()) { + // The stop condition is not true + return; + } + currentManagedBlockStopConditionHasBecomeTrue = true; + serverThread.signal(null); + } + + // Gale start - base thread pool + private final PackRepository packRepository; private final ServerScoreboard scoreboard; @Nullable @@ -287,7 +393,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop processQueue = new java.util.concurrent.ConcurrentLinkedQueue(); public int autosavePeriod; public Commands vanillaCommandDispatcher; - public boolean forceTicks; // Paper + public volatile boolean forceTicks; // Paper // Gale - base thread pool - make fields volatile // CraftBukkit end // Spigot start public static final int TPS = 20; @@ -303,9 +409,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop S spin(Function serverFactory) { + public static S spin(Function serverFactory) { // Gale - base thread pool AtomicReference atomicreference = new AtomicReference(); - Thread thread = new io.papermc.paper.util.TickThread(() -> { // Paper - rewrite chunk system + OriginalServerThread thread = new OriginalServerThread(() -> { // Paper - rewrite chunk system // Gale - base thread pool ((MinecraftServer) atomicreference.get()).runServer(); }, "Server thread"); @@ -324,16 +430,19 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop holdergetter = this.registries.compositeAccess().registryOrThrow(Registries.BLOCK).asLookup().filterFeatures(this.worldData.enabledFeatures()); this.structureTemplateManager = new StructureTemplateManager(worldstem.resourceManager(), convertable_conversionsession, datafixer, holdergetter); - this.serverThread = thread; + // Gale start - base thread pool + serverThread = thread; + BaseThreadActivation.callForUpdate(); + // Gale end - base thread pool this.executor = Util.backgroundExecutor(); } // CraftBukkit start @@ -599,7 +711,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop public public volatile boolean hasFullyShutdown = false; // Paper private boolean hasLoggedStop = false; // Paper private final Object stopLock = new Object(); @@ -916,8 +1029,10 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop Util.getMillis() - startTime >= 30 || !BaseTaskQueues.scheduledAsync.hasTasks(), null); // Paper + LOGGER.info("Shutting down IO executor..."); + // Gale end - base thread pool - remove Paper async executor + // Gale end - base thread pool - remove background executor Util.shutdownExecutors(); // Paper LOGGER.info("Closing Server"); try { @@ -1017,7 +1131,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop public // Paper start if (this.forceTicks) { return true; @@ -1253,13 +1406,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop public private boolean canOversleep() { - return this.mayHaveDelayedTasks && Util.getMillis() < this.delayedTasksMaxNextTickTime; + return Util.getMillis() < this.delayedTasksMaxNextTickTime && mayHaveDelayedTasks(); // Gale - base thread pool } private boolean canSleepForTickNoOversleep() { @@ -1268,7 +1421,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { return !this.canSleepForTickNoOversleep(); // Paper - move oversleep into full server tick + // Gale start - base thread pool }); + isInSpareTime = false; + isWaitingUntilNextTick = false; + // Gale end - base thread pool lastTickOversleepTime = (System.nanoTime() - tickOversleepStart) / 1000000L; // Gale - YAPFA - last tick time } - @Override - public TickTask wrapRunnable(Runnable runnable) { - // Paper start - anything that does try to post to main during watchdog crash, run on watchdog - if (this.hasStopped && Thread.currentThread().equals(shutdownThread)) { - runnable.run(); - runnable = () -> {}; - } - // Paper end - return new TickTask(this.tickCount, runnable); - } - - protected boolean shouldRun(TickTask ticktask) { - return ticktask.getTick() + 3 < this.tickCount || this.haveTime(); - } - - @Override - public boolean pollTask() { - boolean flag = this.pollTaskInternal(); - - this.mayHaveDelayedTasks = flag; - return flag; - } - - private boolean pollTaskInternal() { - if (super.pollTask()) { - this.executeMidTickTasks(); // Paper - execute chunk tasks mid tick - return true; - } else { - boolean ret = false; // Paper - force execution of all worlds, do not just bias the first - if (this.haveTime()) { - Iterator iterator = this.getAllLevels().iterator(); - - while (iterator.hasNext()) { - ServerLevel worldserver = (ServerLevel) iterator.next(); - - if (worldserver.getChunkSource().pollTask()) { - ret = true; // Paper - force execution of all worlds, do not just bias the first - } - } - } - - return ret; // Paper - force execution of all worlds, do not just bias the first - } - } - - public void doRunTask(TickTask ticktask) { // CraftBukkit - decompile error - super.doRunTask(ticktask); - } - private void updateStatusIcon(ServerStatus metadata) { Optional optional = Optional.of(this.getFile("server-icon.png")).filter(File::isFile); @@ -1378,14 +1490,19 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { return !this.canOversleep(); + // Gale start - base thread pool }); + isInSpareTime = false; + // Gale end - base thread pool isOversleep = false;MinecraftTimings.serverOversleep.stopTiming(); // Paper end new com.destroystokyo.paper.event.server.ServerTickStartEvent(this.tickCount+1).callEvent(); // Paper ++this.tickCount; + ScheduledServerThreadTaskQueues.shiftTasksForNextTick(); // Gale - base thread pool this.tickChildren(shouldKeepTicking); if (i - this.lastServerStatus >= 5000000000L) { this.lastServerStatus = i; @@ -1420,7 +1537,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0) { this.playerList.saveAll(playerSaveInterval); } - for (ServerLevel level : this.getAllLevels()) { + for (ServerLevel level : this.getAllLevelsArray()) { // Gale - base thread pool - optimize server levels if (level.paperConfig().chunks.autoSaveInterval.value() > 0) { level.saveIncrementally(fullSave); } @@ -1432,7 +1549,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0; // Paper worldserver.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper net.minecraft.world.level.block.entity.HopperBlockEntity.skipHopperEvents = worldserver.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper @@ -1569,7 +1684,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop, ServerLevel> newLevels = Maps.newLinkedHashMap(oldLevels); newLevels.put(level.dimension(), level); this.levels = Collections.unmodifiableMap(newLevels); + // Gale start - base thread pool - optimize server levels + this.levelArray = newLevels.values().toArray(this.levelArray); + for (int i = 0; i < this.levelArray.length; i++) { + this.levelArray[i].serverLevelArrayIndex = i; + } + this.overworld = null; + // Gale end - base thread pool - optimize server levels } public void removeLevel(ServerLevel level) { @@ -1598,6 +1725,14 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop, ServerLevel> newLevels = Maps.newLinkedHashMap(oldLevels); newLevels.remove(level.dimension()); this.levels = Collections.unmodifiableMap(newLevels); + // Gale start - base thread pool - optimize server levels + level.serverLevelArrayIndex = -1; + this.levelArray = newLevels.values().toArray(this.levelArray); + for (int i = 0; i < this.levelArray.length; i++) { + this.levelArray[i].serverLevelArrayIndex = i; + } + this.overworld = null; + // Gale end - base thread pool - optimize server levels } // CraftBukkit end @@ -1605,8 +1740,14 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop getAllLevels() { - return this.levels.values(); + return this.levels == null ? Collections.emptyList() : this.levels.values(); // Gale - base thread pool } public String getServerVersion() { @@ -1726,10 +1867,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop players; public final ServerChunkCache chunkSource; + // Gale start - base thread pool + @AnyThreadSafe(Access.READ) + public volatile int serverLevelArrayIndex; + // Gale end - base thread pool private final MinecraftServer server; public final PrimaryLevelData serverLevelData; // CraftBukkit - type final EntityTickList entityTickList; @@ -2558,7 +2566,7 @@ public class ServerLevel extends Level implements WorldGenLevel { // Spigot start if ( entity instanceof Player ) { - com.google.common.collect.Streams.stream( ServerLevel.this.getServer().getAllLevels() ).map( ServerLevel::getDataStorage ).forEach( (worldData) -> + Arrays.stream( ServerLevel.this.getServer().getAllLevelsArray() ).map( ServerLevel::getDataStorage ).forEach( (worldData) -> // Gale - base thread pool - optimize server levels { for (Object o : worldData.cache.values() ) { diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java index 14ee62567ace6fc1becf4257761a811d2ab6f71d..93a533bdeb5108f5c3e758f8062de083283fd075 100644 --- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java @@ -185,8 +185,9 @@ import net.minecraft.world.phys.shapes.BooleanOp; import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; import org.bukkit.craftbukkit.util.permissions.CraftDefaultPermissions; -import org.apache.commons.lang3.StringUtils; import org.galemc.gale.configuration.GaleGlobalConfiguration; +import org.galemc.gale.executor.queue.BaseTaskQueues; +import org.galemc.gale.executor.queue.ScheduledServerThreadTaskQueues; import org.slf4j.Logger; // CraftBukkit start @@ -552,7 +553,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic Objects.requireNonNull(this.connection); // CraftBukkit - Don't wait - minecraftserver.scheduleOnMain(networkmanager::handleDisconnection); // Paper + ScheduledServerThreadTaskQueues.add(networkmanager::handleDisconnection, ScheduledServerThreadTaskQueues.HANDLE_DISCONNECT_TASK_MAX_DELAY); // Paper // Gale - base thread pool } private CompletableFuture filterTextPacket(T text, BiFunction> filterer) { @@ -883,21 +884,20 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic } // Paper start - private static final java.util.concurrent.ExecutorService TAB_COMPLETE_EXECUTOR = java.util.concurrent.Executors.newFixedThreadPool(4, - new com.google.common.util.concurrent.ThreadFactoryBuilder().setDaemon(true).setNameFormat("Async Tab Complete Thread - #%d").setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(net.minecraft.server.MinecraftServer.LOGGER)).build()); + private static final java.util.concurrent.ExecutorService TAB_COMPLETE_EXECUTOR = BaseTaskQueues.scheduledAsync.yieldingExecutor; // Gale - base thread pool - remove tab complete executor // Paper end @Override public void handleCustomCommandSuggestions(ServerboundCommandSuggestionPacket packet) { // PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel()); // Paper - run this async // CraftBukkit start if (this.chatSpamTickCount.addAndGet(io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.tabSpamIncrement) > io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.tabSpamLimit && !this.server.getPlayerList().isOp(this.player.getGameProfile())) { // Paper start - split and make configurable - server.scheduleOnMain(() -> this.disconnect(Component.translatable("disconnect.spam", ArrayConstants.emptyObjectArray), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM)); // Paper - kick event cause // Gale - JettPack - reduce array allocations + ScheduledServerThreadTaskQueues.add(() -> this.disconnect(Component.translatable("disconnect.spam", ArrayConstants.emptyObjectArray), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM), ScheduledServerThreadTaskQueues.KICK_FOR_COMMAND_PACKET_SPAM_TASK_MAX_DELAY); // Paper - kick event cause // Gale - JettPack - reduce array allocations // Gale - base thread pool return; } // Paper start String str = packet.getCommand(); int index = -1; if (str.length() > 64 && ((index = str.indexOf(' ')) == -1 || index >= 64)) { - server.scheduleOnMain(() -> this.disconnect(Component.translatable("disconnect.spam", ArrayConstants.emptyObjectArray), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM)); // Paper - kick event cause // Gale - JettPack - reduce array allocations + ScheduledServerThreadTaskQueues.add(() -> this.disconnect(Component.translatable("disconnect.spam", ArrayConstants.emptyObjectArray), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM), ScheduledServerThreadTaskQueues.KICK_FOR_COMMAND_PACKET_SPAM_TASK_MAX_DELAY); // Paper - kick event cause // Gale - JettPack - reduce array allocations // Gale - base thread pool return; } // Paper end @@ -922,7 +922,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic if (!event.isHandled()) { if (!event.isCancelled()) { - this.server.scheduleOnMain(() -> { // This needs to be on main + ScheduledServerThreadTaskQueues.add(() -> { // This needs to be on main // Gale - base thread pool ParseResults parseresults = this.server.getCommands().getDispatcher().parse(stringreader, this.player.createCommandSourceStack()); this.server.getCommands().getDispatcher().getCompletionSuggestions(parseresults).thenAccept((suggestions) -> { @@ -933,7 +933,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic this.connection.send(new ClientboundCommandSuggestionsPacket(packet.getId(), suggestEvent.getSuggestions())); // Paper end - Brigadier API }); - }); + }, ScheduledServerThreadTaskQueues.SEND_COMMAND_COMPLETION_SUGGESTIONS_TASK_MAX_DELAY); // Gale - base thread pool } } else if (!completions.isEmpty()) { final com.mojang.brigadier.suggestion.SuggestionsBuilder builder0 = new com.mojang.brigadier.suggestion.SuggestionsBuilder(command, stringreader.getTotalLength()); @@ -1247,7 +1247,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic int byteLength = testString.getBytes(java.nio.charset.StandardCharsets.UTF_8).length; if (byteLength > 256 * 4) { ServerGamePacketListenerImpl.LOGGER.warn(this.player.getScoreboardName() + " tried to send a book with with a page too large!"); - server.scheduleOnMain(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause + ScheduledServerThreadTaskQueues.add(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION), ScheduledServerThreadTaskQueues.KICK_FOR_BOOK_TOO_LARGE_PACKET_TASK_MAX_DELAY); // Paper - kick event cause // Gale - base thread pool return; } byteTotal += byteLength; @@ -1270,14 +1270,14 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic if (byteTotal > byteAllowed) { ServerGamePacketListenerImpl.LOGGER.warn(this.player.getScoreboardName() + " tried to send too large of a book. Book Size: " + byteTotal + " - Allowed: "+ byteAllowed + " - Pages: " + pageList.size()); - server.scheduleOnMain(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause + ScheduledServerThreadTaskQueues.add(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION), ScheduledServerThreadTaskQueues.KICK_FOR_BOOK_TOO_LARGE_PACKET_TASK_MAX_DELAY); // Paper - kick event cause // Gale - base thread pool return; } } // Paper end // CraftBukkit start if (this.lastBookTick + 20 > MinecraftServer.currentTick) { - server.scheduleOnMain(() -> this.disconnect("Book edited too quickly!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause // Paper - Also ensure this is called on main + ScheduledServerThreadTaskQueues.add(() -> this.disconnect("Book edited too quickly!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION), ScheduledServerThreadTaskQueues.KICK_FOR_EDITING_BOOK_TOO_QUICKLY_TASK_MAX_DELAY); // Paper - kick event cause // Paper - Also ensure this is called on main // Gale - base thread pool return; } this.lastBookTick = MinecraftServer.currentTick; @@ -2081,10 +2081,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic public void handleTeleportToEntityPacket(ServerboundTeleportToEntityPacket packet) { PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel()); if (this.player.isSpectator()) { - Iterator iterator = this.server.getAllLevels().iterator(); - - while (iterator.hasNext()) { - ServerLevel worldserver = (ServerLevel) iterator.next(); + for (ServerLevel worldserver : this.server.getAllLevelsArray()) { // Gale - base thread pool - optimize server levels Entity entity = packet.getEntity(worldserver); if (entity != null) { @@ -2233,9 +2230,9 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic } // CraftBukkit end if (ServerGamePacketListenerImpl.isChatMessageIllegal(packet.message())) { - this.server.scheduleOnMain(() -> { // Paper - push to main for event firing + ScheduledServerThreadTaskQueues.add(() -> { // Paper - push to main for event firing // Gale - base thread pool this.disconnect(Component.translatable("multiplayer.disconnect.illegal_characters"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_CHARACTERS); // Paper - add cause - }); // Paper - push to main for event firing + }, ScheduledServerThreadTaskQueues.KICK_FOR_ILLEGAL_CHARACTERS_IN_CHAT_PACKET_TASK_MAX_DELAY); // Paper - push to main for event firing // Gale - base thread pool } else { Optional optional = this.tryHandleChat(packet.message(), packet.timeStamp(), packet.lastSeenMessages()); @@ -2269,9 +2266,9 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic @Override public void handleChatCommand(ServerboundChatCommandPacket packet) { if (ServerGamePacketListenerImpl.isChatMessageIllegal(packet.command())) { - this.server.scheduleOnMain(() -> { // Paper - push to main for event firing + ScheduledServerThreadTaskQueues.add(() -> { // Paper - push to main for event firing // Gale - base thread pool this.disconnect(Component.translatable("multiplayer.disconnect.illegal_characters"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_CHARACTERS); // Paper - }); // Paper - push to main for event firing + }, ScheduledServerThreadTaskQueues.KICK_FOR_ILLEGAL_CHARACTERS_IN_CHAT_PACKET_TASK_MAX_DELAY); // Paper - push to main for event firing // Gale - base thread pool } else { Optional optional = this.tryHandleChat(packet.command(), packet.timeStamp(), packet.lastSeenMessages()); @@ -2353,9 +2350,9 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic private Optional tryHandleChat(String message, Instant timestamp, LastSeenMessages.Update acknowledgment) { if (!this.updateChatOrder(timestamp)) { if (GaleGlobalConfiguration.get().logToConsole.chat.outOfOrderMessageWarning) ServerGamePacketListenerImpl.LOGGER.warn("{} sent out-of-order chat: '{}': {} > {}", this.player.getName().getString(), message, this.lastChatTimeStamp.get().getEpochSecond(), timestamp.getEpochSecond()); // Paper // Gale - do not log out-of-order message warnings - this.server.scheduleOnMain(() -> { // Paper - push to main - this.disconnect(Component.translatable("multiplayer.disconnect.out_of_order_chat"), org.bukkit.event.player.PlayerKickEvent.Cause.OUT_OF_ORDER_CHAT); // Paper - kick event ca - }); // Paper - push to main + ScheduledServerThreadTaskQueues.add(() -> { // Paper - push to main // Gale - base thread pool + this.disconnect(Component.translatable("multiplayer.disconnect.out_of_order_chat"), org.bukkit.event.player.PlayerKickEvent.Cause.OUT_OF_ORDER_CHAT); // Paper - kick event cause + }, ScheduledServerThreadTaskQueues.KICK_FOR_OUT_OF_ORDER_CHAT_PACKET_TASK_MAX_DELAY); // Paper - push to main // Gale - base thread pool return Optional.empty(); } else if (this.player.isRemoved() || this.player.getChatVisibility() == ChatVisiblity.HIDDEN) { // CraftBukkit - dead men tell no tales this.send(new ClientboundSystemChatPacket(Component.translatable("chat.disabled.options").withStyle(ChatFormatting.RED), false)); @@ -3290,7 +3287,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic // Paper start if (!org.bukkit.Bukkit.isPrimaryThread()) { if (recipeSpamPackets.addAndGet(io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.recipeSpamIncrement) > io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.recipeSpamLimit) { - server.scheduleOnMain(() -> this.disconnect(net.minecraft.network.chat.Component.translatable("disconnect.spam", ArrayConstants.emptyObjectArray), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM)); // Paper - kick event cause // Gale - JettPack - reduce array allocations + ScheduledServerThreadTaskQueues.add(() -> this.disconnect(net.minecraft.network.chat.Component.translatable("disconnect.spam", ArrayConstants.emptyObjectArray), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM), ScheduledServerThreadTaskQueues.KICK_FOR_RECIPE_PACKET_SPAM_TASK_MAX_DELAY); // Paper - kick event cause // Gale - JettPack - reduce array allocations // Gale - base thread pool return; } } diff --git a/src/main/java/net/minecraft/server/network/TextFilterClient.java b/src/main/java/net/minecraft/server/network/TextFilterClient.java index 4b3d2280326c7eeda4952c36edff141cbff90e16..e684fa1990d631cafd8e84debe52301fc9ed329f 100644 --- a/src/main/java/net/minecraft/server/network/TextFilterClient.java +++ b/src/main/java/net/minecraft/server/network/TextFilterClient.java @@ -23,7 +23,6 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; import javax.annotation.Nullable; @@ -32,6 +31,7 @@ import net.minecraft.Util; import net.minecraft.network.chat.FilterMask; import net.minecraft.util.GsonHelper; import net.minecraft.util.thread.ProcessorMailbox; +import org.galemc.gale.executor.queue.BaseTaskQueues; import org.slf4j.Logger; public class TextFilterClient implements AutoCloseable { @@ -62,7 +62,7 @@ public class TextFilterClient implements AutoCloseable { this.joinEncoder = joinEncoder; this.leaveEndpoint = leaveEndpoint; this.leaveEncoder = leaveEncoder; - this.workerPool = Executors.newFixedThreadPool(parallelism, THREAD_FACTORY); + this.workerPool = BaseTaskQueues.scheduledAsync.yieldingExecutor; // Gale - base thread pool - remove text filter executor } private static URL getEndpoint(URI root, @Nullable JsonObject endpoints, String key, String fallback) throws MalformedURLException { diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java index 6f139e6cbb61bfb2be9b8b886bec7cddbb2c8993..0cbef825129b173a5244a195ea68444c216c0b1b 100644 --- a/src/main/java/net/minecraft/server/players/PlayerList.java +++ b/src/main/java/net/minecraft/server/players/PlayerList.java @@ -15,7 +15,6 @@ import java.net.SocketAddress; import java.nio.file.Path; import java.text.SimpleDateFormat; import java.time.Instant; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.EnumSet; @@ -105,10 +104,10 @@ import net.minecraft.world.scores.PlayerTeam; import net.minecraft.world.scores.Scoreboard; // Paper import net.minecraft.world.scores.Team; import org.galemc.gale.configuration.GaleGlobalConfiguration; +import org.galemc.gale.executor.queue.ScheduledServerThreadTaskQueues; import org.slf4j.Logger; // CraftBukkit start -import java.util.stream.Collectors; import net.minecraft.server.dedicated.DedicatedServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; @@ -255,7 +254,7 @@ public abstract class PlayerList { // Gale start - MultiPaper - do not place player in world if kicked before being spawned in if (!connection.isConnected() || player.quitReason != null) { - pendingPlayers.remove(player.getUUID(), player); + /*pendingPlayers.remove(player.getUUID(), player);*/ // Gale - base thread pool - this patch was removed from Paper but might be useful later return; } // Gale end - MultiPaper - do not place player in world if kicked before being spawned in @@ -296,6 +295,58 @@ public abstract class PlayerList { player.getRecipeBook().sendInitialRecipeBook(player); this.updateEntireScoreboard(worldserver1.getScoreboard(), player); this.server.invalidateStatus(); +/* // Gale - base thread pool - this patch was removed from Paper but might be useful later + // Paper start - async load spawn in chunk + ServerLevel finalWorldserver = worldserver1; + finalWorldserver.pendingLogin.add(player); + int chunkX = loc.getBlockX() >> 4; + int chunkZ = loc.getBlockZ() >> 4; + final net.minecraft.world.level.ChunkPos pos = new net.minecraft.world.level.ChunkPos(chunkX, chunkZ); + net.minecraft.server.level.ChunkMap playerChunkMap = worldserver1.getChunkSource().chunkMap; + net.minecraft.server.level.DistanceManager distanceManager = playerChunkMap.distanceManager; + io.papermc.paper.chunk.system.ChunkSystem.scheduleTickingState( + worldserver1, chunkX, chunkZ, net.minecraft.server.level.ChunkHolder.FullChunkStatus.ENTITY_TICKING, true, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHEST, + (chunk) -> { + ScheduledServerThreadTaskQueues.add(() -> { // Gale - base thread pool + try { + if (!playerconnection.connection.isConnected()) { + return; + } + PlayerList.this.postChunkLoadJoin( + player, finalWorldserver, connection, playerconnection, + nbttagcompound, s1, lastKnownName + ); + distanceManager.addTicket(net.minecraft.server.level.TicketType.LOGIN, pos, 31, pos.toLong()); + } finally { + finalWorldserver.pendingLogin.remove(player); + } + }, ScheduledServerThreadTaskQueues.POST_CHUNK_LOAD_JOIN_TASK_MAX_DELAY); // Gale - base thread pool + } + ); + } + + public ServerPlayer getActivePlayer(UUID uuid) { + ServerPlayer player = this.playersByUUID.get(uuid); + return player != null ? player : pendingPlayers.get(uuid); + } + + void disconnectPendingPlayer(ServerPlayer entityplayer) { + Component msg = Component.translatable("multiplayer.disconnect.duplicate_login"); + entityplayer.networkManager.send(new net.minecraft.network.protocol.game.ClientboundDisconnectPacket(msg), net.minecraft.network.PacketSendListener.thenRun(() -> { + entityplayer.networkManager.disconnect(msg); + entityplayer.networkManager = null; + })); + } + + private void postChunkLoadJoin(ServerPlayer player, ServerLevel worldserver1, Connection networkmanager, ServerGamePacketListenerImpl playerconnection, CompoundTag nbttagcompound, String s1, String s) { + pendingPlayers.remove(player.getUUID(), player); + if (!networkmanager.isConnected()) { + return; + } + player.didPlayerJoinEvent = true; + // Paper end +*/ // Gale - base thread pool - this patch was removed from Paper but might be useful later MutableComponent ichatmutablecomponent; if (player.getGameProfile().getName().equalsIgnoreCase(s)) { @@ -1523,10 +1574,8 @@ public abstract class PlayerList { public void setViewDistance(int viewDistance) { this.viewDistance = viewDistance; //this.broadcastAll(new ClientboundSetChunkCacheRadiusPacket(viewDistance)); // Paper - move into setViewDistance - Iterator iterator = this.server.getAllLevels().iterator(); - while (iterator.hasNext()) { - ServerLevel worldserver = (ServerLevel) iterator.next(); + for (ServerLevel worldserver : this.server.getAllLevelsArray()) { // Gale - base thread pool - optimize server levels if (worldserver != null) { worldserver.getChunkSource().setViewDistance(viewDistance); @@ -1538,10 +1587,8 @@ public abstract class PlayerList { public void setSimulationDistance(int simulationDistance) { this.simulationDistance = simulationDistance; //this.broadcastAll(new ClientboundSetSimulationDistancePacket(simulationDistance)); // Paper - handled by playerchunkloader - Iterator iterator = this.server.getAllLevels().iterator(); - while (iterator.hasNext()) { - ServerLevel worldserver = (ServerLevel) iterator.next(); + for (ServerLevel worldserver : this.server.getAllLevelsArray()) { // Gale - base thread pool - optimize server levels if (worldserver != null) { worldserver.getChunkSource().setSimulationDistance(simulationDistance); diff --git a/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java b/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java index 83701fbfaa56a232593ee8f11a3afb8941238bfa..392e7b4a89669f16b32043b65b69e6593d17f10e 100644 --- a/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java +++ b/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java @@ -6,17 +6,18 @@ import com.mojang.logging.LogUtils; import java.util.List; import java.util.Queue; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; import java.util.concurrent.locks.LockSupport; import java.util.function.BooleanSupplier; import java.util.function.Supplier; + import net.minecraft.util.profiling.metrics.MetricCategory; import net.minecraft.util.profiling.metrics.MetricSampler; import net.minecraft.util.profiling.metrics.MetricsRegistry; import net.minecraft.util.profiling.metrics.ProfilerMeasured; +import org.galemc.gale.executor.AbstractBlockableEventLoop; import org.slf4j.Logger; -public abstract class BlockableEventLoop implements ProfilerMeasured, ProcessorHandle, Executor { +public abstract class BlockableEventLoop implements ProfilerMeasured, ProcessorHandle, AbstractBlockableEventLoop { // Gale - base thread pool private final String name; private static final Logger LOGGER = LogUtils.getLogger(); private final Queue pendingRunnables = Queues.newConcurrentLinkedQueue(); @@ -31,6 +32,7 @@ public abstract class BlockableEventLoop implements Profiler protected abstract boolean shouldRun(R task); + @Override // Gale - base thread pool public boolean isSameThread() { return Thread.currentThread() == this.getRunningThread(); } @@ -45,6 +47,12 @@ public abstract class BlockableEventLoop implements Profiler return this.pendingRunnables.size(); } + // Gale start - base thread pool + public boolean hasPendingTasks() { + return !this.pendingRunnables.isEmpty(); + } + // Gale end - base thread pool + @Override public String name() { return this.name; @@ -102,6 +110,7 @@ public abstract class BlockableEventLoop implements Profiler } + @Override // Gale - base thread pool public void executeIfPossible(Runnable runnable) { this.execute(runnable); } diff --git a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java index 9948cc4c65d5681c171b38cdf7cf3e63a01e4364..cba854bdcab80ba411096ef4fd97e46861764d48 100644 --- a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java +++ b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java @@ -98,7 +98,7 @@ public abstract class Projectile extends Entity { this.cachedOwner = ((ServerLevel) this.level).getEntity(this.ownerUUID); // Paper start - check all worlds if (this.cachedOwner == null) { - for (final ServerLevel level : this.level.getServer().getAllLevels()) { + for (final ServerLevel level : this.level.getServer().getAllLevelsArray()) { // Gale - base thread pool - optimize server levels if (level == this.level) continue; final Entity entity = level.getEntity(this.ownerUUID); if (entity != null) { diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java index e23fdd5ba09b50b7eef0ca4f36c5480779fba624..a7bb3275b2da8308696b18fb527514f9c4859d35 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java @@ -986,7 +986,7 @@ public final class CraftServer implements Server { org.spigotmc.SpigotConfig.init((File) console.options.valueOf("spigot-settings")); // Spigot this.console.paperConfigurations.reloadConfigs(this.console); this.console.galeConfigurations.reloadConfigs(this.console); // Gale - Gale configuration - for (ServerLevel world : this.console.getAllLevels()) { + for (ServerLevel world : this.console.getAllLevelsArray()) { // Gale - base thread pool - optimize server levels // world.serverLevelData.setDifficulty(config.difficulty); // Paper - per level difficulty world.setSpawnSettings(world.serverLevelData.getDifficulty() != Difficulty.PEACEFUL && config.spawnMonsters, config.spawnAnimals); // Paper - per level difficulty (from MinecraftServer#setDifficulty(ServerLevel, Difficulty, boolean)) @@ -1170,7 +1170,7 @@ public final class CraftServer implements Server { @Override public World createWorld(WorldCreator creator) { - Preconditions.checkState(this.console.getAllLevels().iterator().hasNext(), "Cannot create additional worlds on STARTUP"); + Preconditions.checkState(this.console.getAllLevelsArray().length > 0, "Cannot create additional worlds on STARTUP"); // Gale - base thread pool - optimize server levels //Preconditions.checkState(!this.console.isIteratingOverLevels, "Cannot create a world while worlds are being ticked"); // Paper - Cat - Temp disable. We'll see how this goes. Validate.notNull(creator, "Creator may not be null"); @@ -2526,7 +2526,7 @@ public final class CraftServer implements Server { public Entity getEntity(UUID uuid) { Validate.notNull(uuid, "UUID cannot be null"); - for (ServerLevel world : this.getServer().getAllLevels()) { + for (ServerLevel world : this.getServer().getAllLevelsArray()) { // Gale - base thread pool - optimize server levels net.minecraft.world.entity.Entity entity = world.getEntity(uuid); if (entity != null) { return entity.getBukkitEntity(); diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java index 4cb0307935aa63d44aac55c80ee50be074d7913c..949feba1264bcafb8dc2dcecd0a566fea80a2ba0 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java @@ -5,7 +5,6 @@ import com.google.common.base.Predicates; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.mojang.datafixers.util.Pair; -import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import java.io.File; @@ -20,7 +19,6 @@ import java.util.Objects; import java.util.Random; import java.util.Set; import java.util.UUID; -import java.util.concurrent.ExecutionException; import java.util.function.Predicate; import java.util.stream.Collectors; import net.minecraft.core.BlockPos; @@ -114,7 +112,6 @@ import org.bukkit.entity.TippedArrow; import org.bukkit.entity.Trident; import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason; import org.bukkit.event.weather.LightningStrikeEvent; -import org.bukkit.event.world.SpawnChangeEvent; import org.bukkit.event.world.TimeSkipEvent; import org.bukkit.generator.BiomeProvider; import org.bukkit.generator.BlockPopulator; @@ -134,6 +131,7 @@ import org.bukkit.util.Consumer; import org.bukkit.util.RayTraceResult; import org.bukkit.util.StructureSearchResult; import org.bukkit.util.Vector; +import org.galemc.gale.executor.queue.ScheduledServerThreadTaskQueues; public class CraftWorld extends CraftRegionAccessor implements World { public static final int CUSTOM_DIMENSION_OFFSET = 10; @@ -2356,11 +2354,11 @@ public class CraftWorld extends CraftRegionAccessor implements World { java.util.concurrent.CompletableFuture ret = new java.util.concurrent.CompletableFuture<>(); io.papermc.paper.chunk.system.ChunkSystem.scheduleChunkLoad(this.getHandle(), x, z, gen, ChunkStatus.FULL, true, priority, (c) -> { - net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> { + ScheduledServerThreadTaskQueues.add(() -> { // Gale - base thread pool net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk)c; if (chunk != null) addTicket(x, z); // Paper ret.complete(chunk == null ? null : chunk.getBukkitChunk()); - }); + }, ScheduledServerThreadTaskQueues.COMPLETE_CHUNK_FUTURE_TASK_MAX_DELAY); // Gale - base thread pool }); return ret; diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java index 78f53ee557276de85f0431ebcb146445b1f4fb92..6176867eea06c53882dcaacfbde0334b39b903cc 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java @@ -190,6 +190,7 @@ import org.bukkit.plugin.Plugin; import org.bukkit.util.BoundingBox; import org.bukkit.util.NumberConversions; import org.bukkit.util.Vector; +import org.galemc.gale.executor.queue.ScheduledServerThreadTaskQueues; public abstract class CraftEntity implements org.bukkit.entity.Entity { private static PermissibleBase perm; @@ -1280,7 +1281,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { for (net.minecraft.world.level.chunk.ChunkAccess chunk : list) { chunkProviderServer.addTicketAtLevel(net.minecraft.server.level.TicketType.POST_TELEPORT, chunk.getPos(), 33, CraftEntity.this.getEntityId()); } - net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> { + ScheduledServerThreadTaskQueues.add(() -> { // Gale - base thread pool try { ret.complete(CraftEntity.this.teleport(locationClone, cause) ? Boolean.TRUE : Boolean.FALSE); } catch (Throwable throwable) { @@ -1290,7 +1291,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { net.minecraft.server.MinecraftServer.LOGGER.error("Failed to teleport entity " + CraftEntity.this, throwable); ret.completeExceptionally(throwable); } - }); + }, ScheduledServerThreadTaskQueues.TELEPORT_ASYNC_TASK_MAX_DELAY); // Gale - base thread pool }); return ret; diff --git a/src/main/java/org/galemc/gale/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 9571aae593999d11b3908856b0295a7d6b588007..ed2841d3a6c6d90ad02266f38c0821bca4f549f1 100644 --- a/src/main/java/org/galemc/gale/configuration/GaleConfigurations.java +++ b/src/main/java/org/galemc/gale/configuration/GaleConfigurations.java @@ -264,7 +264,7 @@ public class GaleConfigurations extends Configurations + *
  • Default: -1
  • + *
  • Vanilla: -1
  • + * + */ + @Setting("default") + public int defaultValue = -1; + + /** + * The default maximum delay for completing a {@link java.util.concurrent.CompletableFuture} + * for a chunk load, after the chunk has already finished loading. + * Given in ticks. + * Any value < 0 uses {@link #defaultValue}. + *
      + *
    • Default: 0
    • + *
    • Vanilla: -1
    • + *
    + */ + public int completeChunkFuture = 0; + + /** + * The default maximum delay for completing the steps needed to take when a player is joining and the + * necessary chunk has been loaded. + * Given in ticks. + * Any value < 0 uses {@link #defaultValue}. + *
      + *
    • Default: 19
    • + *
    • Vanilla: -1
    • + *
    + */ + public int postChunkLoadJoin = 19; + + /** + * The default maximum delay for chunk packets to be modified for anti-xray. + * Given in ticks. + * Any value < 0 uses {@link #defaultValue}. + *
      + *
    • Default: 19
    • + *
    • Vanilla: -1
    • + *
    + */ + public int antiXrayModifyBlocks = 19; + + /** + * The default maximum delay for entities to be teleported when a teleport is started asynchronously. + * Given in ticks. + * Any value < 0 uses {@link #defaultValue}. + *
      + *
    • Default: -1
    • + *
    • Vanilla: -1
    • + *
    + */ + public int teleportAsync = -1; + + /** + * The default maximum delay for command completion suggestions to be sent to the player. + * Any value < 0 uses {@link #defaultValue}. + *
      + *
    • Default: 9
    • + *
    • Vanilla: -1
    • + *
    + */ + public int sendCommandCompletionSuggestions = 9; + + /** + * The default maximum delay for players to get kicked for command packet spam. + * Given in ticks. + * Any value < 0 uses {@link #defaultValue}. + *
      + *
    • Default: 0
    • + *
    • Vanilla: -1
    • + *
    + */ + public int kickForCommandPacketSpam = 0; + + /** + * The default maximum delay for players to get kicked for place-recipe packet spam. + * Given in ticks. + * Any value < 0 uses {@link #defaultValue}. + *
      + *
    • Default: 0
    • + *
    • Vanilla: -1
    • + *
    + */ + public int kickForRecipePacketSpam = 0; + + /** + * The default maximum delay for players to get kicked for sending invalid packets trying to + * send book content that is too large, which usually indicates they are attempting to abuse an exploit. + * Given in ticks. + * Any value < 0 uses {@link #defaultValue}. + *
      + *
    • Default: -1
    • + *
    • Vanilla: -1
    • + *
    + */ + public int kickForBookTooLargePacket = -1; + + /** + * The default maximum delay for players to get kicked for editing a book too quickly. + * Given in ticks. + * Any value < 0 uses {@link #defaultValue}. + *
      + *
    • Default: -1
    • + *
    • Vanilla: -1
    • + *
    + */ + public int kickForEditingBookTooQuickly = -1; + + /** + * The default maximum delay for players to get kicked for sending a chat packet with illegal characters. + * Given in ticks. + * Any value < 0 uses {@link #defaultValue}. + *
      + *
    • Default: -1
    • + *
    • Vanilla: -1
    • + *
    + */ + public int kickForIllegalCharactersInChatPacket = -1; + + /** + * The default maximum delay for players to get kicked for sending an out-of-order chat packet. + * Given in ticks. + * Any value < 0 uses {@link #defaultValue}. + *
      + *
    • Default: -1
    • + *
    • Vanilla: -1
    • + *
    + */ + public int kickForOutOfOrderChatPacket = -1; + + /** + * The default maximum delay for handling player disconnects. + * Any value < 0 uses {@link #defaultValue}. + *
      + *
    • Default: -1
    • + *
    • Vanilla: -1
    • + *
    + */ + public int handleDisconnect = -1; + + @Override + public void postProcess() { + while (!ScheduledServerThreadTaskQueues.writeLock.tryLock()); + try { + // Update the values in MinecraftServerBlockableEventLoop for quick access + ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY = this.defaultValue >= 0 ? this.defaultValue : 2; + ScheduledServerThreadTaskQueues.COMPLETE_CHUNK_FUTURE_TASK_MAX_DELAY = this.completeChunkFuture >= 0 ? this.completeChunkFuture : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledServerThreadTaskQueues.POST_CHUNK_LOAD_JOIN_TASK_MAX_DELAY = this.postChunkLoadJoin >= 0 ? this.postChunkLoadJoin : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledServerThreadTaskQueues.ANTI_XRAY_MODIFY_BLOCKS_TASK_MAX_DELAY = this.antiXrayModifyBlocks >= 0 ? this.antiXrayModifyBlocks : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledServerThreadTaskQueues.TELEPORT_ASYNC_TASK_MAX_DELAY = this.teleportAsync >= 0 ? this.teleportAsync : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledServerThreadTaskQueues.SEND_COMMAND_COMPLETION_SUGGESTIONS_TASK_MAX_DELAY = this.sendCommandCompletionSuggestions >= 0 ? this.sendCommandCompletionSuggestions : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledServerThreadTaskQueues.KICK_FOR_COMMAND_PACKET_SPAM_TASK_MAX_DELAY = this.kickForCommandPacketSpam >= 0 ? this.kickForCommandPacketSpam : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledServerThreadTaskQueues.KICK_FOR_RECIPE_PACKET_SPAM_TASK_MAX_DELAY = this.kickForRecipePacketSpam >= 0 ? this.kickForRecipePacketSpam : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledServerThreadTaskQueues.KICK_FOR_BOOK_TOO_LARGE_PACKET_TASK_MAX_DELAY = this.kickForBookTooLargePacket >= 0 ? this.kickForBookTooLargePacket : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledServerThreadTaskQueues.KICK_FOR_EDITING_BOOK_TOO_QUICKLY_TASK_MAX_DELAY = this.kickForEditingBookTooQuickly >= 0 ? this.kickForEditingBookTooQuickly : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledServerThreadTaskQueues.KICK_FOR_ILLEGAL_CHARACTERS_IN_CHAT_PACKET_TASK_MAX_DELAY = this.kickForIllegalCharactersInChatPacket >= 0 ? this.kickForIllegalCharactersInChatPacket : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledServerThreadTaskQueues.KICK_FOR_OUT_OF_ORDER_CHAT_PACKET_TASK_MAX_DELAY = this.kickForOutOfOrderChatPacket >= 0 ? this.kickForOutOfOrderChatPacket : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + ScheduledServerThreadTaskQueues.HANDLE_DISCONNECT_TASK_MAX_DELAY = this.handleDisconnect >= 0 ? this.handleDisconnect : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY; + // Change the length of the pendingRunnables array of queues + int maxDelay = 0; + for (int delay : new int[]{ + ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY, + ScheduledServerThreadTaskQueues.COMPLETE_CHUNK_FUTURE_TASK_MAX_DELAY, + ScheduledServerThreadTaskQueues.POST_CHUNK_LOAD_JOIN_TASK_MAX_DELAY, + ScheduledServerThreadTaskQueues.ANTI_XRAY_MODIFY_BLOCKS_TASK_MAX_DELAY, + ScheduledServerThreadTaskQueues.TELEPORT_ASYNC_TASK_MAX_DELAY, + ScheduledServerThreadTaskQueues.SEND_COMMAND_COMPLETION_SUGGESTIONS_TASK_MAX_DELAY, + ScheduledServerThreadTaskQueues.KICK_FOR_COMMAND_PACKET_SPAM_TASK_MAX_DELAY, + ScheduledServerThreadTaskQueues.KICK_FOR_RECIPE_PACKET_SPAM_TASK_MAX_DELAY, + ScheduledServerThreadTaskQueues.KICK_FOR_BOOK_TOO_LARGE_PACKET_TASK_MAX_DELAY, + ScheduledServerThreadTaskQueues.KICK_FOR_EDITING_BOOK_TOO_QUICKLY_TASK_MAX_DELAY, + ScheduledServerThreadTaskQueues.KICK_FOR_ILLEGAL_CHARACTERS_IN_CHAT_PACKET_TASK_MAX_DELAY, + ScheduledServerThreadTaskQueues.KICK_FOR_OUT_OF_ORDER_CHAT_PACKET_TASK_MAX_DELAY, + ScheduledServerThreadTaskQueues.HANDLE_DISCONNECT_TASK_MAX_DELAY + }) { + if (delay > maxDelay) { + maxDelay = delay; + } + } + int newPendingRunnablesLength = maxDelay + 1; + int oldPendingRunnablesLength = ScheduledServerThreadTaskQueues.queues.length; + if (oldPendingRunnablesLength != newPendingRunnablesLength) { + if (oldPendingRunnablesLength > newPendingRunnablesLength) { + // Move all tasks in queues that will be removed to the last queue + for (int i = newPendingRunnablesLength + 1; i < ScheduledServerThreadTaskQueues.queues.length; i++) { + ScheduledServerThreadTaskQueues.queues[maxDelay].addAll(ScheduledServerThreadTaskQueues.queues[i]); + } + // Update the first queue with elements index + if (ScheduledServerThreadTaskQueues.firstQueueWithPotentialTasksIndex >= newPendingRunnablesLength) { + ScheduledServerThreadTaskQueues.firstQueueWithPotentialTasksIndex = maxDelay; + } + } + ScheduledServerThreadTaskQueues.queues = Arrays.copyOf(ScheduledServerThreadTaskQueues.queues, newPendingRunnablesLength); + if (newPendingRunnablesLength > oldPendingRunnablesLength) { + // Create new queues + for (int i = oldPendingRunnablesLength; i < newPendingRunnablesLength; i++) { + ScheduledServerThreadTaskQueues.queues[i] = new MultiThreadedQueue<>(); + } + } + } + } finally { + ScheduledServerThreadTaskQueues.writeLock.unlock(); + } + } + + } + // Gale end - base thread pool + } public GameplayMechanics gameplayMechanics; diff --git a/src/main/java/org/galemc/gale/executor/AbstractBlockableEventLoop.java b/src/main/java/org/galemc/gale/executor/AbstractBlockableEventLoop.java new file mode 100644 index 0000000000000000000000000000000000000000..e9d778a078bee6b6f1c21078c445b48fc276e985 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/AbstractBlockableEventLoop.java @@ -0,0 +1,21 @@ +// Gale - base thread pool + +package org.galemc.gale.executor; + +import net.minecraft.util.thread.BlockableEventLoop; + +import java.util.concurrent.Executor; + +/** + * An interface for the common functionality of {@link BlockableEventLoop} and {@link MinecraftServerBlockableEventLoop}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public interface AbstractBlockableEventLoop extends Executor { + + boolean isSameThread(); + + @SuppressWarnings("unused") + void executeIfPossible(Runnable runnable); + +} diff --git a/src/main/java/org/galemc/gale/executor/MinecraftServerBlockableEventLoop.java b/src/main/java/org/galemc/gale/executor/MinecraftServerBlockableEventLoop.java new file mode 100644 index 0000000000000000000000000000000000000000..84aa4f8e3f823cedc8cf958663fa2168f29d1d9c --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/MinecraftServerBlockableEventLoop.java @@ -0,0 +1,187 @@ +// Gale - base thread pool + +package org.galemc.gale.executor; + +import com.mojang.logging.LogUtils; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.thread.BlockableEventLoop; +import net.minecraft.util.thread.ProcessorHandle; +import net.minecraft.util.thread.ReentrantBlockableEventLoop; +import org.galemc.gale.executor.annotation.thread.ServerThreadOnly; +import org.galemc.gale.executor.queue.BaseTaskQueues; +import org.galemc.gale.executor.queue.ScheduledServerThreadTaskQueues; +import org.galemc.gale.executor.thread.ServerThread; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.RejectedExecutionException; +import java.util.function.BooleanSupplier; +import java.util.function.Supplier; + +/** + * This is a base class for {@link MinecraftServer}, as a replacement of {@link BlockableEventLoop} + * (and the intermediary class {@link ReentrantBlockableEventLoop}). + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public class MinecraftServerBlockableEventLoop implements ProcessorHandle, AbstractBlockableEventLoop { + + private static final String NAME = "Server"; + private static final Logger LOGGER = LogUtils.getLogger(); + + public static volatile int blockingCount; + private static volatile int reentrantCount; + + public static boolean scheduleExecutables() { + return (reentrantCount != 0 || Thread.currentThread() != ServerThread.getInstance()) && !MinecraftServer.SERVER.isStopped(); + } + + protected boolean runningTask() { + return reentrantCount != 0; + } + + public CompletableFuture submit(Supplier task) { + return scheduleExecutables() ? CompletableFuture.supplyAsync(task, this) : CompletableFuture.completedFuture(task.get()); + } + + private CompletableFuture submitAsync(Runnable runnable) { + return CompletableFuture.supplyAsync(() -> { + runnable.run(); + return null; + }, this); + } + + public CompletableFuture submit(Runnable task) { + if (scheduleExecutables()) { + return this.submitAsync(task); + } else { + task.run(); + return CompletableFuture.completedFuture(null); + } + } + + public void executeBlocking(Runnable runnable) { + if (Thread.currentThread() != ServerThread.getInstance()) { + this.submitAsync(runnable).join(); + } else { + runnable.run(); + } + } + + /** + * @deprecated Use {@link ScheduledServerThreadTaskQueues#add(Runnable, int)} instead: + * do not rely on {@link ScheduledServerThreadTaskQueues#DEFAULT_TASK_MAX_DELAY}. + */ + @Deprecated + @Override + public void tell(@NotNull Runnable message) { + ScheduledServerThreadTaskQueues.add(() -> { + //noinspection NonAtomicOperationOnVolatileField + ++reentrantCount; + try { + message.run(); + } catch (Exception var3) { + if (var3.getCause() instanceof ThreadDeath) throw var3; // Paper + LOGGER.error(LogUtils.FATAL_MARKER, "Error executing task on {}", NAME, var3); + } finally { + //noinspection NonAtomicOperationOnVolatileField + --reentrantCount; + if (MinecraftServer.isWaitingUntilNextTick) { + MinecraftServer.signalServerThreadIfCurrentManagedBlockStopConditionBecameTrue(); + } + } + MinecraftServer.SERVER.executeMidTickTasks(); // Paper - execute chunk tasks mid tick + }); + } + + @Override + public void execute(@NotNull Runnable var1) { + if (scheduleExecutables()) { + this.tell(var1); + } else { + var1.run(); + } + } + + @Override + public boolean isSameThread() { + return Thread.currentThread() == MinecraftServer.serverThread; + } + + @Override + public void executeIfPossible(Runnable runnable) { + if (MinecraftServer.SERVER.isStopped()) { + throw new RejectedExecutionException("Server already shutting down"); + } else { + this.execute(runnable); + } + } + + /** + * Runs all tasks, regardless of which tick they must be finished in, or whether there is time. + */ + @ServerThreadOnly + protected void runAllMainThreadTasksForAllTicks() { + Runnable task; + while (true) { + // Force polling every tasks regardless of the tick they have to be finished by + MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking = false; + task = ScheduledServerThreadTaskQueues.poll(ServerThread.getInstance(), true); + if (task == null) { + break; + } + task.run(); + } + } + + /** + * Runs all tasks while there is time. + * Runs at least all tasks that must be finished in the current tick, regardless of whether there is time. + */ + @ServerThreadOnly + protected void runAllTasksWithinTimeOrForCurrentTick() { + Runnable task; + while (true) { + /* + Update this value accurately: we are in 'spare time' here, we may have more time or not, and we are + definitely not already blocking. + */ + MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking = !MinecraftServer.SERVER.haveTime(); + task = BaseTaskQueues.anyTickScheduledServerThread.poll(ServerThread.getInstance()); + if (task == null) { + break; + } + task.run(); + } + } + + @ServerThreadOnly + public void managedBlock(@NotNull BooleanSupplier stopCondition) { + MinecraftServer.currentManagedBlockStopCondition = stopCondition; + try { + // Check stop condition beforehand to prevent unnecessarily releasing main thread + MinecraftServer.currentManagedBlockStopConditionHasBecomeTrue = false; + if (stopCondition.getAsBoolean()) { + MinecraftServer.currentManagedBlockStopConditionHasBecomeTrue = true; + return; + } + //noinspection NonAtomicOperationOnVolatileField + ++blockingCount; + try { + MinecraftServer.serverThread.runTasksUntil(stopCondition, null); + } finally { + //noinspection NonAtomicOperationOnVolatileField + --blockingCount; + } + } finally { + MinecraftServer.currentManagedBlockStopCondition = null; + } + } + + @Override + public @NotNull String name() { + return NAME; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/TaskSpan.java b/src/main/java/org/galemc/gale/executor/TaskSpan.java new file mode 100644 index 0000000000000000000000000000000000000000..1d5bb1ba545200f954c886a2afb9d8ee2a3cc4d1 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/TaskSpan.java @@ -0,0 +1,62 @@ +// Gale - base thread pool + +package org.galemc.gale.executor; + +/** + * An enum for the behaviour of a task, in terms of its potential to yield + * and its expected time cost to finish. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public enum TaskSpan { + + /** + * Indicates that a task that may potentially yield. + * The expected duration of this task is not specifically established. + */ + YIELDING(true), + /** + * Indicates that a task is yield-free. + * The expected duration of this task is not specifically established. + */ + FREE(false), + /** + * Indicates that a task is yield-free, + * and has an expected running time that is below double the approximate time cost of two context switches. + * It is assumed that a context switch takes approximately 5 microseconds, and as such, + * this span indicates that a task has an expected running time of 20 or fewer microseconds. + */ + TINY(false); + + /** + * Equal to {@link #ordinal()}. + */ + public final int ordinal; + + /** + * Whether tasks with this span are potentially yielding. + */ + public final boolean isYielding; + + /** + * Equal to the negation of {@link #isYielding}. + */ + public final boolean isNotYielding; + + TaskSpan(boolean isYielding) { + this.ordinal = this.ordinal(); + this.isYielding = isYielding; + this.isNotYielding = !this.isYielding; + } + + /** + * Equal to {@link #values()}. + */ + public static final TaskSpan[] VALUES = values(); + + /** + * Equal to {@link #VALUES}{@code .length}. + */ + public static final int length = VALUES.length; + +} diff --git a/src/main/java/org/galemc/gale/executor/annotation/Access.java b/src/main/java/org/galemc/gale/executor/annotation/Access.java index d07f68ff73a368c8f0da56152021a95474a601ca..50541414e1d91ff06d108d9b3fe64dcb4ad09668 100644 --- a/src/main/java/org/galemc/gale/executor/annotation/Access.java +++ b/src/main/java/org/galemc/gale/executor/annotation/Access.java @@ -27,13 +27,13 @@ public enum Access { WRITE, /** * Both {@link #READ} and {@link #WRITE}: if the annotation is applied to a field, it holds for both access to - * the field's value, as well as modifications made to the field. + * the field's value, and for modifications made to the field. *
    * This may or may not extend to conceptual access and/or modifications. * * @see #READ * @see #WRITE */ - READ_WRITE; + READ_WRITE } diff --git a/src/main/java/org/galemc/gale/executor/annotation/Guarded.java b/src/main/java/org/galemc/gale/executor/annotation/Guarded.java index 84a0bac98a382550c826e6adbecec1fe7be974a1..6f1d1960953daf7f6f61643f5165e9a0760a647e 100644 --- a/src/main/java/org/galemc/gale/executor/annotation/Guarded.java +++ b/src/main/java/org/galemc/gale/executor/annotation/Guarded.java @@ -24,7 +24,7 @@ import java.lang.annotation.Target; * @author Martijn Muijsers under AGPL-3.0 */ @Documented -@Repeatable +@Repeatable(Guarded.Container.class) @Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE}) public @interface Guarded { @@ -44,4 +44,12 @@ public @interface Guarded { */ String except() default ""; + @Documented + @Target(ElementType.ANNOTATION_TYPE) + @interface Container { + + Guarded[] value(); + + } + } diff --git a/src/main/java/org/galemc/gale/executor/annotation/PotentiallyBlocking.java b/src/main/java/org/galemc/gale/executor/annotation/PotentiallyBlocking.java index 71f26852c96dea34ea07efe07f834f8262509957..d324c303245bcbedaaaab573803d73caff941901 100644 --- a/src/main/java/org/galemc/gale/executor/annotation/PotentiallyBlocking.java +++ b/src/main/java/org/galemc/gale/executor/annotation/PotentiallyBlocking.java @@ -2,7 +2,7 @@ package org.galemc.gale.executor.annotation; -import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.AbstractYieldingThread; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; @@ -19,7 +19,7 @@ import java.lang.annotation.Target; * {@link PotentiallyBlocking}, {@link PotentiallyYielding} or {@link YieldFree} may all be used. *
    * Methods that are potentially blocking, including those annotated with {@link PotentiallyBlocking}, must never - * be called on a {@link BaseThread}. + * be called on an {@link AbstractYieldingThread}. * * @author Martijn Muijsers under AGPL-3.0 */ diff --git a/src/main/java/org/galemc/gale/executor/annotation/PotentiallyYielding.java b/src/main/java/org/galemc/gale/executor/annotation/PotentiallyYielding.java index e87ee2612348fc559b21256cc7cadfc684f01f8e..7ff4e4ab43d316e319efb33b2dd365d679a58118 100644 --- a/src/main/java/org/galemc/gale/executor/annotation/PotentiallyYielding.java +++ b/src/main/java/org/galemc/gale/executor/annotation/PotentiallyYielding.java @@ -2,6 +2,9 @@ package org.galemc.gale.executor.annotation; +import org.galemc.gale.executor.lock.YieldingLock; +import org.galemc.gale.executor.thread.AbstractYieldingThread; + import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Target; @@ -16,6 +19,9 @@ import java.lang.annotation.Target; *
    * In a method annotated with {@link PotentiallyYielding}, the only methods that can be called are those * annotated with {@link PotentiallyYielding} or {@link YieldFree}. + *
    + * It should be assumed that any method annotated with {@link PotentiallyYielding} is potentially blocking if used + * on a thread that is not a {@link AbstractYieldingThread}. * * @author Martijn Muijsers under AGPL-3.0 */ diff --git a/src/main/java/org/galemc/gale/executor/annotation/thread/AssistThreadOnly.java b/src/main/java/org/galemc/gale/executor/annotation/thread/AssistThreadOnly.java new file mode 100644 index 0000000000000000000000000000000000000000..203799c5a9ddec3e665a7476f5e48a2c0f457b04 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/annotation/thread/AssistThreadOnly.java @@ -0,0 +1,37 @@ +// Gale - thread-safety annotations + +package org.galemc.gale.executor.annotation.thread; + +import org.galemc.gale.executor.annotation.Access; +import org.galemc.gale.executor.annotation.PotentiallyBlocking; +import org.galemc.gale.executor.thread.AssistThread; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * An annotation primarily for methods, identifying methods that can only be called on a thread that is an instance + * of {@link AssistThread}. + *
    + * This annotation can also be used on fields or classes, similar to {@link ThreadRestricted}. + *
    + * In a method annotated with {@link AssistThreadOnly}, fields and methods annotated with + * {@link AssistThreadOnly}, {@link BaseThreadOnly} or {@link AnyThreadSafe} may be used. + *
    + * Methods that are annotated with {@link AssistThreadOnly} must never call methods that are annotated with + * {@link PotentiallyBlocking}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@SuppressWarnings("unused") +@Documented +@Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD}) +public @interface AssistThreadOnly { + + /** + * @see ThreadRestricted#fieldAccess() + */ + Access value() default Access.READ_WRITE; + +} diff --git a/src/main/java/org/galemc/gale/executor/annotation/thread/BaseThreadOnly.java b/src/main/java/org/galemc/gale/executor/annotation/thread/BaseThreadOnly.java new file mode 100644 index 0000000000000000000000000000000000000000..e682953181dbf208e731ab5b081664a129210310 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/annotation/thread/BaseThreadOnly.java @@ -0,0 +1,36 @@ +// Gale - thread-safety annotations + +package org.galemc.gale.executor.annotation.thread; + +import org.galemc.gale.executor.annotation.Access; +import org.galemc.gale.executor.annotation.PotentiallyBlocking; +import org.galemc.gale.executor.thread.BaseThread; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * An annotation primarily for methods, identifying methods that can only be called on a thread that is an instance + * of {@link BaseThread}. + *
    + * This annotation can also be used on fields or classes, similar to {@link ThreadRestricted}. + *
    + * In a method annotated with {@link BaseThreadOnly}, fields and methods annotated with + * {@link BaseThreadOnly} or {@link AnyThreadSafe} may be used. + *
    + * Methods that are annotated with {@link BaseThreadOnly} must never call methods that are annotated with + * {@link PotentiallyBlocking}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@Documented +@Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD}) +public @interface BaseThreadOnly { + + /** + * @see ThreadRestricted#fieldAccess() + */ + Access value() default Access.READ_WRITE; + +} diff --git a/src/main/java/org/galemc/gale/executor/annotation/thread/OriginalServerThreadOnly.java b/src/main/java/org/galemc/gale/executor/annotation/thread/OriginalServerThreadOnly.java new file mode 100644 index 0000000000000000000000000000000000000000..d6476e007de11fb4e556cee6ec6eea107dc814fa --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/annotation/thread/OriginalServerThreadOnly.java @@ -0,0 +1,39 @@ +// Gale - thread-safety annotations + +package org.galemc.gale.executor.annotation.thread; + +import org.galemc.gale.executor.annotation.Access; +import org.galemc.gale.executor.annotation.PotentiallyBlocking; +import org.galemc.gale.executor.thread.OriginalServerThread; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * An annotation primarily for methods, identifying methods that can only be called from the + * {@link OriginalServerThread}. + *
    + * This annotation can also be used on fields, similar to {@link ThreadRestricted}. + *
    + * In a method annotated with {@link OriginalServerThreadOnly}, fields and methods annotated with + * {@link OriginalServerThreadOnly}, {@link ServerThreadOnly}, {@link BaseThreadOnly} + * or {@link AnyThreadSafe} may be used. + *
    + * Methods that are annotated with {@link OriginalServerThreadOnly} must never call methods that are annotated with + * {@link PotentiallyBlocking}. + * + * @see ThreadRestricted + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@Documented +@Target({ElementType.METHOD, ElementType.FIELD}) +public @interface OriginalServerThreadOnly { + + /** + * @see ThreadRestricted#fieldAccess() + */ + Access value() default Access.READ_WRITE; + +} diff --git a/src/main/java/org/galemc/gale/executor/annotation/thread/ServerThreadOnly.java b/src/main/java/org/galemc/gale/executor/annotation/thread/ServerThreadOnly.java new file mode 100644 index 0000000000000000000000000000000000000000..d27ee27a65635d0136c5c9e33925b64036ae5cd3 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/annotation/thread/ServerThreadOnly.java @@ -0,0 +1,37 @@ +// Gale - thread-safety annotations + +package org.galemc.gale.executor.annotation.thread; + +import org.galemc.gale.executor.annotation.Access; +import org.galemc.gale.executor.annotation.PotentiallyBlocking; +import org.galemc.gale.executor.thread.ServerThread; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * An annotation primarily for methods, identifying methods that can only be called from a {@link ServerThread}. + *
    + * This annotation can also be used on fields or classes, similar to {@link ThreadRestricted}. + *
    + * In a method annotated with {@link ServerThreadOnly}, fields and methods annotated with + * {@link ServerThreadOnly}, {@link BaseThreadOnly} or {@link AnyThreadSafe} may be used. + *
    + * Methods that are annotated with {@link ServerThreadOnly} must never call methods that are annotated with + * {@link PotentiallyBlocking}. + * + * @see ThreadRestricted + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@Documented +@Target({ElementType.METHOD, ElementType.FIELD}) +public @interface ServerThreadOnly { + + /** + * @see ThreadRestricted#fieldAccess() + */ + Access value() default Access.READ_WRITE; + +} diff --git a/src/main/java/org/galemc/gale/executor/lock/CheckableLock.java b/src/main/java/org/galemc/gale/executor/lock/CheckableLock.java new file mode 100644 index 0000000000000000000000000000000000000000..995746e7f89481f885ea6e3965fc9a2f3f9e9498 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/lock/CheckableLock.java @@ -0,0 +1,20 @@ +package org.galemc.gale.executor.lock; + +import org.galemc.gale.executor.annotation.YieldFree; + +import java.util.concurrent.locks.Lock; + +/** + * A {@link Lock} that also provides an {@link #isLocked()} method. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public interface CheckableLock extends Lock { + + /** + * @return Whether this lock is currently held. + */ + @YieldFree + boolean isLocked(); + +} diff --git a/src/main/java/org/galemc/gale/executor/lock/MultipleWaitingBaseThreadsYieldingLock.java b/src/main/java/org/galemc/gale/executor/lock/MultipleWaitingBaseThreadsYieldingLock.java new file mode 100644 index 0000000000000000000000000000000000000000..a248a9ea644a8bb4175da2e1903483ab6866bc48 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/lock/MultipleWaitingBaseThreadsYieldingLock.java @@ -0,0 +1,39 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.lock; + +import org.galemc.gale.executor.thread.BaseThread; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; + +/** + * A {@link YieldingLock} for which multiple {@link BaseThread}s may be waiting at the same time. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@SuppressWarnings("unused") +public class MultipleWaitingBaseThreadsYieldingLock extends YieldingLock { + + private final AtomicInteger waitingThreadCount = new AtomicInteger(); + + public MultipleWaitingBaseThreadsYieldingLock(Lock innerLock) { + super(innerLock); + } + + @Override + public void incrementWaitingThreads() { + this.waitingThreadCount.incrementAndGet(); + } + + @Override + public void decrementWaitingThreads() { + this.waitingThreadCount.decrementAndGet(); + } + + @Override + protected boolean hasWaitingThreads() { + return this.waitingThreadCount.get() > 0; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/lock/YieldingLock.java b/src/main/java/org/galemc/gale/executor/lock/YieldingLock.java new file mode 100644 index 0000000000000000000000000000000000000000..ad3a7e94824c32c812e6ca57cc6cea78227eac5f --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/lock/YieldingLock.java @@ -0,0 +1,152 @@ +// Gale - base thread pool + +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.pool.BaseThreadActivation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * A wrapper for a lock that can be acquired, but if not able to be acquired right away, can cause the current thread + * to perform other tasks, attempting to acquire the lock again at a later time. + *
    + * The lock is reentrant if the underlying controlled lock is. + *
    + * The lock only be speculatively acquired from any {@link AbstractYieldingThread}. + * Acquiring it on a thread that is not an {@link AbstractYieldingThread} will perform regular locking + * on the underlying controlled lock, which typically blocks the thread. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@AnyThreadSafe +public abstract class YieldingLock implements CheckableLock { + + private final Lock innerLock; + + /** + * The same value as {@link #lock}, or null if it is not an instance of {@link CheckableLock}. + */ + private final @Nullable CheckableLock innerCheckableLock; + + /** + * The same value as {@link #lock}, or null if it is not an instance of {@link ReentrantLock}. + */ + private final @Nullable ReentrantLock innerReentrantLock; + + public YieldingLock(Lock innerLock) { + this.innerLock = innerLock; + if (innerLock instanceof CheckableLock checkableLock) { + this.innerCheckableLock = checkableLock; + this.innerReentrantLock = null; + } else if (innerLock instanceof ReentrantLock reentrantLock) { + this.innerCheckableLock = null; + this.innerReentrantLock = reentrantLock; + } else { + throw new IllegalArgumentException("The innerLock passed to the YieldingLock() constructor must be an instance of CheckableLock or ReentrantLock"); + } + } + + /** + * Attempts to acquire the lock immediately. + * + * @return Whether the lock was acquired. + */ + @YieldFree + @Override + public boolean tryLock() { + return innerLock.tryLock(); + } + + /** + * Acquires the lock. + *
    + * If the current threads is an {@link AbstractYieldingThread}, + * this will yield to other tasks while the lock can not be acquired. + * Otherwise, this will block until the lock is acquired. + */ + @PotentiallyYielding + @Override + public void lock() { + // Try to acquire the lock straight away + if (!this.innerLock.tryLock()) { + // If unsuccessful, we find out our current thread + AbstractYieldingThread yieldingThread = AbstractYieldingThread.currentYieldingThread(); + // If we are not on a yielding thread, we wait for the lock instead of yielding + if (yieldingThread == null) { + this.innerLock.lock(); + return; + } + // Otherwise, we yield to other tasks until the lock can be acquired + yieldingThread.yieldUntil(null, this); + } + } + + /** + * Releases the lock (must be called after having completed the computation block that required the lock). + */ + @Override + public void unlock() { + this.innerLock.unlock(); + // Potentially signal a thread that this lock has become available. + // Another thread could also acquire the lock at this moment, so when we signal the thread we obtain below, + // it may already be too late for the polled thread to acquire this lock + // (but note that the same thread cannot have been added again because only the thread itself can do that - + // and it is still waiting). + if (this.hasWaitingThreads()) { + BaseThreadActivation.yieldingLockWithWaitingThreadsWasUnlocked(); + } + } + + @SuppressWarnings("RedundantThrows") + @Override + public boolean tryLock(long l, @NotNull TimeUnit timeUnit) throws InterruptedException { + throw new UnsupportedOperationException(); + } + + @SuppressWarnings("RedundantThrows") + @Override + public void lockInterruptibly() throws InterruptedException { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public Condition newCondition() { + // The inner lock may itself not support newCondition and throw UnsupportedOperationException + return this.innerLock.newCondition(); + } + + @Override + public boolean isLocked() { + //noinspection DataFlowIssue + return this.innerCheckableLock != null ? this.innerCheckableLock.isLocked() : this.innerReentrantLock.isLocked(); + } + + /** + * Increments the number of threads waiting for this lock to be released. + */ + @YieldFree + public abstract void incrementWaitingThreads(); + + /** + * Decrements the number of threads waiting for this lock to be released. + */ + @YieldFree + public abstract void decrementWaitingThreads(); + + /** + * @return Whether this lock has any threads waiting for it. + */ + @YieldFree + protected abstract boolean hasWaitingThreads(); + +} 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..552e82a33c59261b06911b479400a7b11965bbd6 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/AbstractTaskQueue.java @@ -0,0 +1,93 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.queue; + +import org.galemc.gale.executor.TaskSpan; +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.annotation.thread.BaseThreadOnly; +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.ServerThread; +import org.jetbrains.annotations.Nullable; + +/** + * An interface for a task queue that may contain tasks of specific {@link TaskSpan}s. + *
    + * All tasks must be non-blocking. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@AnyThreadSafe +@YieldFree +public interface AbstractTaskQueue { + + /** + * @return A name that the queue can be identified by. + */ + String getName(); + + /** + * @return Whether this queue has any tasks at all. + */ + boolean hasTasks(); + + /** + * @return Whether this queue has any tasks with the given {@link TaskSpan}. + */ + boolean hasTasks(TaskSpan span); + + /** + * @return Whether this queue supports tasks of the given {@link TaskSpan} at all. + */ + boolean canHaveTasks(TaskSpan span); + + /** + * Attempts to poll a task. + *
    + * This must not be called on queues in the {@link BaseTaskQueueTier#SERVER} tier by threads that are not an + * instance of {@link ServerThread}. + *
    + * This must not be called on queues apart from the {@link BaseTaskQueueTier#SERVER} tier by threads that are an + * instance of {@link ServerThread}. Use {@link #pollTiny(BaseThread)} instead. + * + * @param currentThread The current thread. + * @return The polled task, or null if this queue was empty. + */ + @BaseThreadOnly + @Nullable Runnable poll(BaseThread currentThread); + + /** + * Attempts to poll a {@link TaskSpan#TINY} task. + * + * @see #poll(BaseThread) + */ + @BaseThreadOnly + @Nullable Runnable pollTiny(BaseThread currentThread); + + /** + * Schedules a new task to this queue. + * + * @param task The task to schedule. + * @param span The {@link TaskSpan} of the task. + */ + void add(Runnable task, TaskSpan span); + + /** + * Sets the tier of this queue. This ensures queues do not accidentally initialize the tiers + * before the tiers have finished initializing themselves. + */ + void setTier(BaseTaskQueueTier tier); + + /** + * @return Whether any of the given task queues is non-empty. + */ + static boolean taskQueuesHaveTasks(AbstractTaskQueue[] queues) { + for (AbstractTaskQueue queue : queues) { + if (queue.hasTasks()) { + return true; + } + } + return false; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledChunkCacheTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledChunkCacheTaskQueue.java new file mode 100644 index 0000000000000000000000000000000000000000..60a3b6e935fcea6cf27c31e2b967bf3758283274 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledChunkCacheTaskQueue.java @@ -0,0 +1,52 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.queue; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import org.galemc.gale.executor.TaskSpan; +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.thread.ServerThread; +import org.jetbrains.annotations.Nullable; + +/** + * This class provides access to, but does not store, the tasks scheduled to be executed on the main thread, + * that are scheduled and normally polled by each world's {@link ServerChunkCache#mainThreadProcessor} in their + * respective {@link ServerChunkCache.MainThreadExecutor#managedBlock}. These tasks could normally also be run in the + * server's {@link MinecraftServer#managedBlock} if there were no more global scheduled server thread tasks, and as + * such we provide access to polling these tasks from a {@link ServerThread}. + *
    + * All tasks provided by this queue must be yield-free. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@AnyThreadSafe +@YieldFree +public final class AllLevelsScheduledChunkCacheTaskQueue extends AllLevelsScheduledTaskQueue { + + AllLevelsScheduledChunkCacheTaskQueue() { + super(TaskSpan.FREE, false); // TODO Gale could be TINY maybe? Should check the type of tasks scheduled + } + + @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..e5c89b7fa702d5c12b4aea0b7464ba8a78317a66 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledTaskQueue.java @@ -0,0 +1,128 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.queue; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import org.galemc.gale.executor.TaskSpan; +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.pool.BaseThreadActivation; +import org.jetbrains.annotations.Nullable; + +/** + * Common implementation for {@link AllLevelsScheduledChunkCacheTaskQueue} and + * {@link AllLevelsScheduledTickThreadChunkTaskQueue}. + *
    + * All tasks provided by this queue must be yield-free. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@AnyThreadSafe +@YieldFree +public abstract class AllLevelsScheduledTaskQueue implements AbstractTaskQueue { + + /** + * The {@link TaskSpan} of tasks in this queue. The span must be yield-free. + */ + public final TaskSpan span; + + /** + * Value of {@code onlyIfLastTimeIsTooLongAgo} in calls to + * {@link BaseThreadActivation#newTaskWasAdded(BaseTaskQueueTier, TaskSpan, boolean)}. + */ + private final boolean onlyNotifyBaseThreadPoolOfNewTasksIfLastTimeIsTooLongAgo; + + /** + * Will be initialized in {@link #setTier}. + */ + private BaseTaskQueueTier tier; + + /** + * An iteration index for iterating over the levels in {@link #poll}. + */ + private int levelIterationIndex; + + protected AllLevelsScheduledTaskQueue(TaskSpan span, boolean onlyNotifyBaseThreadPoolOfNewTasksIfLastTimeIsTooLongAgo) { + this.span = span; + this.onlyNotifyBaseThreadPoolOfNewTasksIfLastTimeIsTooLongAgo = onlyNotifyBaseThreadPoolOfNewTasksIfLastTimeIsTooLongAgo; + } + + protected abstract boolean hasLevelTasks(ServerLevel level); + + protected abstract @Nullable Runnable pollLevel(ServerLevel level); + + @Override + public boolean hasTasks() { + for (ServerLevel level : MinecraftServer.SERVER.getAllLevels()) { + if (this.hasLevelTasks(level)) { + return true; + } + } + return false; + } + + @Override + public boolean hasTasks(TaskSpan span) { + return span == this.span && this.hasTasks(); + } + + @Override + public boolean canHaveTasks(TaskSpan span) { + return span == this.span; + } + + @Override + public @Nullable Runnable poll(BaseThread currentThread) { + // Skip during server bootstrap or if there is no more time in the current spare time + if (MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking || !MinecraftServer.isConstructed) { + return null; + } + ServerLevel[] levels = MinecraftServer.SERVER.getAllLevelsArray(); + int startIndex = this.levelIterationIndex = Math.min(this.levelIterationIndex, levels.length - 1); + // Paper - force execution of all worlds, do not just bias the first + do { + ServerLevel level = levels[this.levelIterationIndex++]; + if (this.levelIterationIndex == levels.length) { + this.levelIterationIndex = 0; + } + if (level.serverLevelArrayIndex != -1) { + Runnable task = this.pollLevel(level); + if (task != null) { + return task; + } + } + } while (this.levelIterationIndex != startIndex); + return null; + } + + @Override + public @Nullable Runnable pollTiny(BaseThread currentThread) { + if (this.span == TaskSpan.TINY) { + return this.poll(currentThread); + } + return null; + } + + @Override + public void add(Runnable task, TaskSpan span) { + throw new UnsupportedOperationException(); + } + + /** + * To be called when a new task has been added to the underlying storage of this queue. + */ + public void newTaskWasAdded() { + BaseThreadActivation.newTaskWasAdded(this.tier, this.span, onlyNotifyBaseThreadPoolOfNewTasksIfLastTimeIsTooLongAgo); + } + + @Override + public void setTier(BaseTaskQueueTier tier) { + if (this.tier != null) { + throw new IllegalStateException(this.getClass().getSimpleName() + ".tier was already initialized"); + } + this.tier = tier; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledTickThreadChunkTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledTickThreadChunkTaskQueue.java new file mode 100644 index 0000000000000000000000000000000000000000..117797c4ac2a81218e9f3e977467b62a18e8136f --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledTickThreadChunkTaskQueue.java @@ -0,0 +1,54 @@ +// Gale - base thread pool + +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.TaskSpan; +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.YieldFree; +import org.jetbrains.annotations.Nullable; + +/** + * This class provides access to, but does not store, the tasks scheduled to be executed on the main thread, + * that are scheduled and normally polled by each world's {@link ServerLevel#chunkTaskScheduler} using + * respective {@link ChunkTaskScheduler#executeMainThreadTask}. These tasks could normally also be run in the + * server's {@link MinecraftServer#managedBlock} or a level's {@link ServerChunkCache}'s + * {@link ServerChunkCache.MainThreadExecutor#managedBlock}, and as such we provide access to polling these tasks + * from a {@link TickThread}. + *
    + * All tasks provided by this queue must be yield-free. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@AnyThreadSafe +@YieldFree +public final class AllLevelsScheduledTickThreadChunkTaskQueue extends AllLevelsScheduledTaskQueue { + + AllLevelsScheduledTickThreadChunkTaskQueue() { + super(TaskSpan.FREE, true); // TODO Gale could be TINY maybe? But probably not? Should check the type of tasks scheduled + } + + @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..690979cb9b7ec3dedbd7d0c45d0c183a2f56d2ec --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/AnyTickScheduledServerThreadTaskQueue.java @@ -0,0 +1,27 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.queue; + +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.YieldFree; + +/** + * This class provides access to, but does not store, the tasks scheduled to be executed on the main thread, + * that must be finished by some time in the future, but not necessarily within the current tick or its spare time. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@AnyThreadSafe +@YieldFree +public final class AnyTickScheduledServerThreadTaskQueue extends TickRequiredScheduledServerThreadTaskQueue { + + AnyTickScheduledServerThreadTaskQueue() { + super(true); + } + + @Override + public String getName() { + return "AnyTickScheduledServerThread"; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/queue/BaseTaskQueueTier.java b/src/main/java/org/galemc/gale/executor/queue/BaseTaskQueueTier.java new file mode 100644 index 0000000000000000000000000000000000000000..bfcec658cbf381cc793d7dd844a81fac27c43337 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/BaseTaskQueueTier.java @@ -0,0 +1,116 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.queue; + +import io.papermc.paper.util.TickThread; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import org.galemc.gale.executor.thread.AssistThread; +import org.galemc.gale.executor.thread.ServerThread; + +import java.util.Arrays; + +/** + * A tier for {@link AbstractTaskQueue}s, that indicates the priority of the tasks in the task queues. + * Every tier contains a list of the queues that are part of the tier. + * The tiers are in order of priority, from high to low. + * Similarly, the queues for each tier are in the same order of priority. + * The tasks in each queue should also be in order of priority whenever relevant, but usually there + * is no strong difference in priority between tasks in the same queue, so they typically operate as FIFO queues, + * so that the longest waiting task implicitly has the highest priority within the queue. + *
    + * Tasks from queues in the {@link #SERVER} tier can only be run on a {@link ServerThread}. + * Tasks from other tiers can be run on {@link ServerThread}s as well as on {@link AssistThread}s. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public enum BaseTaskQueueTier { + + /** + * A tier for queues that contain tasks that must be executed on a {@link ServerThread}. + *
    + * Some parts of the server can only be safely accessed by one thread at a time. + * If they can not be guarded by a lock (or if this is not desired, + * because if a ticking thread would need to acquire this lock it would block it), + * then these parts of the code are typically deferred to the server thread. + * Based on the current use of the {@link TickThread} class, particularly given the existence of + * {@link TickThread#isTickThreadFor(Entity)} and {@link TickThread#isTickThreadFor(ServerLevel, int, int)}, + * we can deduce that future support for performing some of these actions in parallel is planned. + * In such a case, some server thread tasks may become tasks that must be + * executed on an appropriate {@link TickThread}. + * In that case, the queues below should be changed so that the server thread and any of the + * ticking threads poll from queues that contain tasks appropriate for them. + * For example, {@link BaseTaskQueues#deferredToUniversalTickThread} would be for tasks that can run + * on any ticking thread, and additional queues would need to be added concerning a specific + * subject (like an entity or chunk) with tasks that will be run on whichever ticking thread is the + * ticking thread for that subject at the time of polling. + */ + SERVER(new AbstractTaskQueue[]{ + BaseTaskQueues.deferredToServerThread, + BaseTaskQueues.serverThreadTick, + BaseTaskQueues.anyTickScheduledServerThread, + BaseTaskQueues.allLevelsScheduledChunkCache, + BaseTaskQueues.allLevelsScheduledTickThreadChunk + }, MinecraftServer.SERVER_THREAD_PRIORITY), + /** + * A tier for queues that contain tasks that are part of ticking, + * to assist the main ticking thread(s) in doing so. + */ + TICK_ASSIST(new AbstractTaskQueue[]{ + BaseTaskQueues.tickAssist + }, Integer.getInteger("gale.thread.priority.tick", 7)), + /** + * A tier for queues that contain general tasks that must be performed at some point in time, + * asynchronously with respect to the {@link ServerThread} and the ticking of the server. + * Execution of + */ + ASYNC(new AbstractTaskQueue[]{ + // The cleaner queue has high priority because it releases resources back to a pool, thereby saving memory + BaseTaskQueues.cleaner, + BaseTaskQueues.scheduledAsync + }, Integer.getInteger("gale.thread.priority.async", 6)); + + /** + * Equal to {@link #ordinal()}. + */ + public final int ordinal; + + /** + * The task queues that belong to this tier. + */ + public final AbstractTaskQueue[] taskQueues; + + /** + * The priority for threads that are executing a task from this tier. + *
    + * If a thread yields to other tasks, the priority it will have is always the highest priority of any task + * on its stack. + */ + public final int threadPriority; + + BaseTaskQueueTier(AbstractTaskQueue[] taskQueues, int threadPriority) { + this.ordinal = this.ordinal(); + this.taskQueues = taskQueues; + for (AbstractTaskQueue queue : this.taskQueues) { + queue.setTier(this); + } + this.threadPriority = threadPriority; + } + + /** + * Equal to {@link #values()}. + */ + public static final BaseTaskQueueTier[] VALUES = values(); + + /** + * Equal to {@link #VALUES}{@code .length}. + */ + public static final int length = VALUES.length; + + /** + * Equal to {@link #VALUES} without {@link #SERVER}. + */ + public static final BaseTaskQueueTier[] VALUES_EXCEPT_SERVER = Arrays.stream(VALUES).filter(tier -> tier != SERVER).toList().toArray(new BaseTaskQueueTier[length - 1]); + +} diff --git a/src/main/java/org/galemc/gale/executor/queue/BaseTaskQueues.java b/src/main/java/org/galemc/gale/executor/queue/BaseTaskQueues.java new file mode 100644 index 0000000000000000000000000000000000000000..ed3ccf2e64539363a7be2d507c68c40b5913f75c --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/BaseTaskQueues.java @@ -0,0 +1,116 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.queue; + +import io.papermc.paper.util.MCUtil; +import io.papermc.paper.util.TickThread; +import org.galemc.gale.executor.TaskSpan; +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.deferral.TickThreadDeferral; +import org.galemc.gale.executor.thread.AbstractYieldingThread; + +/** + * This class statically provides a list of task queues containing tasks for {@link AbstractYieldingThread}s to poll from. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public final class BaseTaskQueues { + + private BaseTaskQueues() {} + + /** + * This queue stores the tasks scheduled to be executed on a {@link TickThread}, that are procedures that must run + * on a tick thread, but their completion is currently needed by another task that has started running on a thread + * that was not the main thread at the time of scheduling the main-thread-only procedure. + *
    + * These tasks are explicitly those that other tasks are waiting on, and as such always have a higher priority + * in being started than pending tasks in represent steps in ticking the server, and as such always have the + * higher priority in being started than pending tasks in {@link #serverThreadTick}. + *
    + * This queue may contain tasks of every {@link TaskSpan}. + *
    + * This queue's {@link AbstractTaskQueue#add} must not be called from the server thread, + * because the server thread must not defer to itself (because tasks in this queue are assumed to have to run + * independent of other server thread tasks, therefore this will cause a deadlock due to the scheduled task + * never starting). + * Instead, any task that must be deferred to the main thread must instead simply be executed + * when encountered on the main thread. + */ + public static final SimpleTaskQueue deferredToServerThread = SimpleTaskQueue.allSpans("DeferredToServerThread", true); + + /** + * This queue stores the tasks scheduled to be executed on a {@link TickThread}, that are procedures that must run + * on a tick thread, but their completion is currently needed by another task that has started running on a thread + * that was not the main thread at the time of scheduling the main-thread-only procedure. + *
    + * This queue may contain tasks of every {@link TaskSpan}. + *
    + * This is currently completely unused, because {@link TickThreadDeferral} simply adds task to + * {@link #deferredToServerThread} instead, since there are currently no special {@link TickThread}s. + */ + @SuppressWarnings("unused") + public static final SimpleTaskQueue deferredToUniversalTickThread = SimpleTaskQueue.allSpans("DeferredToUniversalTickThread", true); + + /** + * This queue explicitly stores tasks that represent steps or parts of steps in ticking the server and that must be + * executed on the main thread, and as such always have a higher priority in being started than pending tasks in + * {@link #anyTickScheduledServerThread} and {@link #scheduledAsync}. + *
    + * This queue may contain tasks of every {@link TaskSpan}. + *
    + * Tasks in every queue are performed in the order they are added (first-in, first-out). Note that this means + * not all main-thread-only tick tasks are necessarily performed in the order they are added, because they may be + * in different queues: either the queue for potentially yielding tasks or the queue for yield-free tasks. + */ + public static final SimpleTaskQueue serverThreadTick = SimpleTaskQueue.allSpans("ServerThreadTick"); + + /** + * Currently unused: only {@link #anyTickScheduledServerThread} is polled. + * + * @see ThisTickScheduledServerThreadTaskQueue + */ + @SuppressWarnings("unused") + public static final ThisTickScheduledServerThreadTaskQueue thisTickScheduledServerThread = null; + + /** + * @see AnyTickScheduledServerThreadTaskQueue + */ + public static final AnyTickScheduledServerThreadTaskQueue anyTickScheduledServerThread = new AnyTickScheduledServerThreadTaskQueue(); + + /** + * This queue explicitly stores tasks that represent steps or parts of steps in ticking the server that do not have + * to be executed on the main thread (but must be executed on a {@link BaseThread}), and have a higher priority + * in being started than pending tasks in {@link #scheduledAsync}. + *
    + * This queue may contain tasks of every {@link TaskSpan}. + *
    + * Tasks in every queue are performed in the order they are added (first-in, first-out). Note that this means + * not all {@link BaseThread} tick tasks are necessarily performed in the order they are added, because they may be + * in different queues: either the queue for potentially yielding tasks or the queue for yield-free tasks. + */ + public static final SimpleTaskQueue tickAssist = SimpleTaskQueue.allSpans("TickAssist"); + + /** + * @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 SingleSpanSimpleTaskQueue cleaner = SimpleTaskQueue.singleSpan("Cleaner", TaskSpan.TINY); + + /** + * This queue stores the tasks scheduled to be executed on any thread, which would usually be stored in various + * executors with a specific purpose. + *
    + * This queue may contain tasks of every {@link TaskSpan}. + */ + public static final SimpleTaskQueue scheduledAsync = SimpleTaskQueue.allSpans("ScheduledAsync"); + +} 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..3d1a54ad35887e87bf7ad8c75a1f5a9be65c1eb8 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/ScheduledServerThreadTaskQueues.java @@ -0,0 +1,283 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.queue; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.thread.BlockableEventLoop; +import org.galemc.gale.concurrent.UnterminableExecutorService; +import org.galemc.gale.executor.annotation.Access; +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.Guarded; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.thread.pool.BaseThreadActivation; +import org.galemc.gale.executor.thread.ServerThread; +import org.galemc.gale.executor.TaskSpan; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Queue; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * This class stores the tasks scheduled to be executed on the main thread, which would usually be stored in a queue + * in the {@link MinecraftServer} in its role as a {@link BlockableEventLoop}. These tasks are not steps of + * ticking, but other tasks that must be executed at some point while no other main-thread-only task is running. + *
    + * Each task is stored by the number of ticks left before it has to be executed. Tasks will always definitely be + * executed within the required number of ticks. Tasks may also be executed earlier than needed + * if there is time to do so. + *
    + * Note that this means not all tasks are necessarily performed in the order they are added, because they may be in + * different queues based on the number of ticks before they have to be executed. + *
    + * All contained tasks are currently assumed to be {@link TaskSpan#YIELDING}: no special distinction for more + * permissive task spans is made. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@AnyThreadSafe +@YieldFree +public final class ScheduledServerThreadTaskQueues { + + ScheduledServerThreadTaskQueues() {} + + public static int DEFAULT_TASK_MAX_DELAY = 2; + public static int COMPLETE_CHUNK_FUTURE_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY; + public static int POST_CHUNK_LOAD_JOIN_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY; + public static int ANTI_XRAY_MODIFY_BLOCKS_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY; + public static int TELEPORT_ASYNC_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY; + public static int SEND_COMMAND_COMPLETION_SUGGESTIONS_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY; + public static int KICK_FOR_COMMAND_PACKET_SPAM_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY; + public static int KICK_FOR_RECIPE_PACKET_SPAM_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY; + public static int KICK_FOR_BOOK_TOO_LARGE_PACKET_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY; + public static int KICK_FOR_EDITING_BOOK_TOO_QUICKLY_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY; + public static int KICK_FOR_ILLEGAL_CHARACTERS_IN_CHAT_PACKET_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY; + public static int KICK_FOR_OUT_OF_ORDER_CHAT_PACKET_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY; + public static int HANDLE_DISCONNECT_TASK_MAX_DELAY = DEFAULT_TASK_MAX_DELAY; + + /** + * Will be initialized in {@link TickRequiredScheduledServerThreadTaskQueue#setTier}. + */ + static BaseTaskQueueTier tier; + + /** + * A number of queues, with the queue at index i being the queue to be used after another i ticks pass + * even when {@link MinecraftServer#haveTime()} is false. + */ + @SuppressWarnings({"rawtypes", "GrazieInspection"}) + @Guarded(value = "#lock", except = "when optimistically reading using versionStamp as a stamp") + public static Queue[] queues = { new MultiThreadedQueue<>() }; + + /** + * Probably the lowest index of any queue in {@link #queues} that is non-empty. + * This is maintained as well as possible. + */ + public static volatile int firstQueueWithPotentialTasksIndex = 0; + + @Guarded(value = "#lock", fieldAccess = Access.WRITE) + public static volatile long versionStamp = 0; + + private static final ReadWriteLock lock = new ReentrantReadWriteLock(); + private static final Lock readLock = lock.readLock(); + public static final Lock writeLock = lock.writeLock(); + + /** + * @return Whether there are any scheduled main thread tasks, not counting any tasks that do not have to be + * finished within the current tick if {@code tryNonCurrentTickQueuesAtAll} is false. + */ + public static boolean hasTasks(boolean tryNonCurrentTickQueuesAtAll) { + //noinspection StatementWithEmptyBody + while (!readLock.tryLock()); + try { + // Try the queue most likely to contain tasks first + if (firstQueueWithPotentialTasksIndex == 0 || tryNonCurrentTickQueuesAtAll) { + if (!queues[firstQueueWithPotentialTasksIndex].isEmpty()) { + return true; + } + } + int checkUpTo = tryNonCurrentTickQueuesAtAll ? queues.length : 1; + for (int i = 0; i < checkUpTo; i++) { + if (!queues[i].isEmpty()) { + return true; + } + } + } finally { + readLock.unlock(); + } + return false; + } + + /** + * Polls a task from this queue and returns it. + * Tasks that are scheduled for a later tick will only be regarded as able to be started if both we are not out of + * spare time this tick and {@code tryNonCurrentTickQueuesAtAll} is true. + *
    + * This method does not check whether the given thread is or could claim the main thread: whether a task + * can start now is thread-agnostic and based purely on the state of the queue. + * + * @return The task that was polled, or null if no task was found. + */ + public static @Nullable Runnable poll(ServerThread currentThread, boolean tryNonCurrentTickQueuesAtAll) { + // Since we assume the tasks in this queue to be potentially yielding, fail if the thread is restricted + if (!currentThread.canStartYieldingTasks) { + return null; + } + pollFromFirstQueueOrOthers: while (true) { + // Try to get a task from the first queue first + Object task = queues[0].poll(); + if (task != null) { + return (Runnable) task; + } else if (!tryNonCurrentTickQueuesAtAll || MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking) { + // We needed a task from the first queue + if (queues[0].isEmpty()) { + // The first queue really is empty, so we fail + return null; + } + // An element was added to the first queue in the meantime, try again + continue; + } + tryGetRunnableUntilSuccessOrNothingChanged: while (true) { + boolean goOverAllQueues = firstQueueWithPotentialTasksIndex == 0; + long oldVersionStamp = versionStamp; + for (int i = goOverAllQueues ? 0 : firstQueueWithPotentialTasksIndex; i < queues.length; i++) { + if (!queues[i].isEmpty()) { + if (i == 0 || !MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking) { + task = queues[i].poll(); + if (task == null) { + // We lost a race condition between the isEmpty() and poll() calls: just try again + continue tryGetRunnableUntilSuccessOrNothingChanged; + } + return (Runnable) task; + } + // Apparently we must now poll from the first queue only, try again + continue pollFromFirstQueueOrOthers; + } + // The queue was empty, the first queue with potential tasks must be the next one + if (i == firstQueueWithPotentialTasksIndex) { + if (i == queues.length - 1) { + firstQueueWithPotentialTasksIndex = 0; + } else { + firstQueueWithPotentialTasksIndex = i + 1; + } + } + } + if (goOverAllQueues && firstQueueWithPotentialTasksIndex == 0 && oldVersionStamp == versionStamp) { + /* + We went over all queues and nothing changed in the meantime, + we give up, there appear to be no more tasks. + */ + return null; + } + // Something changed, or we did not go over all queues, try again + } + } + } + + public static int getTaskCount() { + //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; + BaseThreadActivation.newTaskWasAdded(tier, TaskSpan.YIELDING); + } + + /** + * 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..2c910f89f1056d00e5e4a2d832cdd4be4b7527b4 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/SimpleTaskQueue.java @@ -0,0 +1,253 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.queue; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import org.galemc.gale.collection.FIFOConcurrentLinkedQueue; +import org.galemc.gale.concurrent.UnterminableExecutorService; +import org.galemc.gale.executor.TaskSpan; +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.pool.BaseThreadActivation; +import org.galemc.gale.executor.thread.SignalReason; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.Queue; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; + +/** + * A base class for a task queue that may contain tasks that are + * potentially yielding and tasks that are yield-free. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@AnyThreadSafe +@YieldFree +public class SimpleTaskQueue implements AbstractTaskQueue { + + /** + * The name of this queue. + * + * @see #getName() + */ + public final String name; + + /** + * The {@link BaseTaskQueueTier} that contains this queue. + * Will be initialized in {@link #setTier} by the constructor of the tier. + */ + public BaseTaskQueueTier tier; + + /** + * Whether tasks in this queue can have a {@link TaskSpan}, indexed by the span's {@link TaskSpan#ordinal}. + */ + public final boolean[] canHaveTasks; + + /** + * The queues of tasks, indexed by their {@link TaskSpan#ordinal}. Individual elements may be null if the + * corresponding {@link #canHaveTasks} value is false. + */ + @SuppressWarnings("rawtypes") + private final @Nullable Queue @NotNull [] queues; + + /** + * An executor for adding tasks of a specific {@link TaskSpan} to this queue, where {@link Executor#execute} + * calls {@link #add(Runnable, TaskSpan)}, + * or null if the corresponding value in {@link #canHaveTasks} is false. + */ + @SuppressWarnings("FieldCanBeLocal") + private final @Nullable ExecutorService @NotNull [] executors; + + /** + * An executor for adding {@link TaskSpan#YIELDING} tasks to this queue, + * where {@link Executor#execute} calls {@link #add}, + * or null if the corresponding value in {@link #canHaveTasks} is false. + */ + public final ExecutorService yieldingExecutor; + + /** + * An executor for adding {@link TaskSpan#FREE} tasks to this queue, + * where {@link Executor#execute} calls {@link #add}, + * or null if the corresponding value in {@link #canHaveTasks} is false. + */ + public final ExecutorService freeExecutor; + + /** + * An executor for adding {@link TaskSpan#TINY} tasks to this queue, + * where {@link Executor#execute} calls {@link #add}, + * or null if the corresponding value in {@link #canHaveTasks} is false. + */ + public final ExecutorService tinyExecutor; + + SimpleTaskQueue(String name, boolean[] canHaveTasks) { + this(name, canHaveTasks, false); + } + + /** + * @param name Value for {@link #getName}. + * @param canHaveTasks Value for {@link #canHaveTasks}. + * @param lifoQueues If true, the queues in {@link #queues} will be LIFO; + * otherwise, they will be FIFO. + */ + SimpleTaskQueue(String name, boolean[] canHaveTasks, boolean lifoQueues) { + this.name = name; + if (canHaveTasks.length != TaskSpan.length) { + throw new IllegalArgumentException(); + } + this.canHaveTasks = canHaveTasks; + this.queues = new Queue[TaskSpan.length]; + this.executors = new ExecutorService[TaskSpan.length]; + for (int spanOrdinal = 0; spanOrdinal < TaskSpan.length; spanOrdinal++) { + if (this.canHaveTasks[spanOrdinal]) { + this.queues[spanOrdinal] = lifoQueues ? new FIFOConcurrentLinkedQueue<>() : new MultiThreadedQueue<>(); + this.executors[spanOrdinal] = new SpanExecutor(TaskSpan.VALUES[spanOrdinal]); + } + } + this.yieldingExecutor = this.executors[TaskSpan.YIELDING.ordinal]; + this.freeExecutor = this.executors[TaskSpan.FREE.ordinal]; + this.tinyExecutor = this.executors[TaskSpan.TINY.ordinal]; + } + + @Override + public final String getName() { + return this.name; + } + + @Override + public final boolean hasTasks() { + for (int spanOrdinal = 0; spanOrdinal < TaskSpan.length; spanOrdinal++) { + var queue = this.queues[spanOrdinal]; + if (queue != null && !queue.isEmpty()) { + return true; + } + } + return false; + } + + @Override + public boolean hasTasks(TaskSpan span) { + var queue = this.queues[span.ordinal]; + return queue != null && !queue.isEmpty(); + } + + @Override + public boolean canHaveTasks(TaskSpan span) { + return this.queues[span.ordinal] != null; + } + + /** + * @see #poll(BaseThread) + */ + private @Nullable Runnable poll(BaseThread currentThread, int spanOrdinal) { + var queue = this.queues[spanOrdinal]; + if (queue != null) { + if (currentThread.canStartYieldingTasks || TaskSpan.VALUES[spanOrdinal].isNotYielding) { + Object task = queue.poll(); + if (task != null) { + /* + If the thread was woken up for a different reason, + another thread should be signalled for that reason. + */ + SignalReason lastSignalReason = currentThread.lastSignalReason; + if (lastSignalReason != null && lastSignalReason != SignalReason.TASK) { + BaseThreadActivation.callForUpdate(); + } + return (Runnable) task; + } + } + } + return null; + } + + @Override + public final @Nullable Runnable poll(BaseThread currentThread) { + Runnable task; + for (int spanOrdinal = 0; spanOrdinal < TaskSpan.length; spanOrdinal++) { + task = this.poll(currentThread, spanOrdinal); + if (task != null) { + return task; + } + } + return null; + } + + @Override + public final @Nullable Runnable pollTiny(BaseThread currentThread) { + return poll(currentThread, TaskSpan.TINY.ordinal); + } + + @Override + public final void add(Runnable task, TaskSpan span) { + int spanOrdinal = span.ordinal; + //noinspection unchecked + this.queues[spanOrdinal].add(task); + BaseThreadActivation.newTaskWasAdded(this.tier, span); + } + + @Override + public final void setTier(BaseTaskQueueTier tier) { + if (this.tier != null) { + throw new IllegalStateException("SimpleTaskQueue.tier was already initialized"); + } + this.tier = tier; + } + + private class SpanExecutor extends UnterminableExecutorService { + + private final TaskSpan span; + + private SpanExecutor(TaskSpan span) { + this.span = span; + } + + @Override + public void execute(@NotNull Runnable runnable) { + SimpleTaskQueue.this.add(runnable, this.span); + } + + } + + public static SingleSpanSimpleTaskQueue singleSpan(String name, TaskSpan span) { + return new SingleSpanSimpleTaskQueue(name, span); + } + + @SuppressWarnings("unused") + public static SingleSpanSimpleTaskQueue singleSpan(String name, TaskSpan span, boolean lifoQueues) { + return new SingleSpanSimpleTaskQueue(name, span, lifoQueues); + } + + private static boolean[] createCanHaveTasksForDoubleSpan(TaskSpan span1, TaskSpan span2) { + boolean[] canHaveTasks = new boolean[TaskSpan.length]; + canHaveTasks[span1.ordinal] = true; + canHaveTasks[span2.ordinal] = true; + return canHaveTasks; + } + + @SuppressWarnings("unused") + public static SimpleTaskQueue doubleSpan(String name, TaskSpan span1, TaskSpan span2) { + return new SimpleTaskQueue(name, createCanHaveTasksForDoubleSpan(span1, span2)); + } + + @SuppressWarnings("unused") + public static SimpleTaskQueue doubleSpan(String name, TaskSpan span1, TaskSpan span2, boolean lifoQueues) { + return new SimpleTaskQueue(name, createCanHaveTasksForDoubleSpan(span1, span2), lifoQueues); + } + + private static final boolean[] canHaveTasksForAllSpans = new boolean[TaskSpan.length]; + static { + Arrays.fill(canHaveTasksForAllSpans, true); + } + + public static SimpleTaskQueue allSpans(String name) { + return new SimpleTaskQueue(name, canHaveTasksForAllSpans); + } + + public static SimpleTaskQueue allSpans(String name, boolean lifoQueues) { + return new SimpleTaskQueue(name, canHaveTasksForAllSpans, lifoQueues); + } + +} diff --git a/src/main/java/org/galemc/gale/executor/queue/SingleSpanSimpleTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/SingleSpanSimpleTaskQueue.java new file mode 100644 index 0000000000000000000000000000000000000000..2251087670d554a7bd5dc81631615aa0728eb315 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/SingleSpanSimpleTaskQueue.java @@ -0,0 +1,66 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.queue; + +import org.galemc.gale.concurrent.UnterminableExecutorService; +import org.galemc.gale.executor.TaskSpan; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; + +/** + * A base class for a task queue that contains tasks that are all of one {@link TaskSpan}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@YieldFree +public class SingleSpanSimpleTaskQueue extends SimpleTaskQueue { + + private static final boolean[][] canHaveTasksPerSpan = new boolean[TaskSpan.length][]; + static { + for (TaskSpan span : TaskSpan.VALUES) { + canHaveTasksPerSpan[span.ordinal] = new boolean[TaskSpan.length]; + canHaveTasksPerSpan[span.ordinal][span.ordinal] = true; + } + } + + private final TaskSpan span; + + SingleSpanSimpleTaskQueue(String name, TaskSpan span) { + super(name, canHaveTasksPerSpan[span.ordinal]); + this.span = span; + } + + SingleSpanSimpleTaskQueue(String name, TaskSpan span, boolean lifoQueues) { + super(name, canHaveTasksPerSpan[span.ordinal], lifoQueues); + this.span = span; + } + + /** + * Schedules a new task to this queue. + * + * @param task The task to schedule. + */ + @AnyThreadSafe + @YieldFree + public void add(Runnable task) { + this.add(task, this.span); + } + + /** + * An executor for adding tasks to this queue, + * where {@link Executor#execute} calls {@link #add}. + */ + public final ExecutorService executor = new UnterminableExecutorService() { + + @Override + public void execute(@NotNull Runnable runnable) { + SingleSpanSimpleTaskQueue.this.add(runnable, SingleSpanSimpleTaskQueue.this.span); + } + + }; + +} diff --git a/src/main/java/org/galemc/gale/executor/queue/ThisTickScheduledServerThreadTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/ThisTickScheduledServerThreadTaskQueue.java new file mode 100644 index 0000000000000000000000000000000000000000..3cb2b84cb7653ff3e038acdc2e6e11f805bfbbba --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/ThisTickScheduledServerThreadTaskQueue.java @@ -0,0 +1,27 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.queue; + +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.YieldFree; + +/** + * This class provides access to, but does not store, the tasks scheduled to be executed on the main thread, + * that must be finished in the current tick or its spare time. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@AnyThreadSafe +@YieldFree +public final class ThisTickScheduledServerThreadTaskQueue extends TickRequiredScheduledServerThreadTaskQueue { + + ThisTickScheduledServerThreadTaskQueue() { + super(false); + } + + @Override + public String getName() { + return "ThisTickScheduledServerThread"; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/queue/TickRequiredScheduledServerThreadTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/TickRequiredScheduledServerThreadTaskQueue.java new file mode 100644 index 0000000000000000000000000000000000000000..fb4f9c047fc71a9a01aa47871254c6a949753a3a --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/queue/TickRequiredScheduledServerThreadTaskQueue.java @@ -0,0 +1,67 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.queue; + +import org.galemc.gale.executor.TaskSpan; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.ServerThread; +import org.jetbrains.annotations.Nullable; + +/** + * A common base class for {@link ThisTickScheduledServerThreadTaskQueue} and + * {@link AnyTickScheduledServerThreadTaskQueue}. + *
    + * This queue does not support {@link #add}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@AnyThreadSafe +@YieldFree +public abstract class TickRequiredScheduledServerThreadTaskQueue implements AbstractTaskQueue { + + private final boolean tryNonCurrentTickQueuesAtAll; + + protected TickRequiredScheduledServerThreadTaskQueue(boolean tryNonCurrentTickQueuesAtAll) { + this.tryNonCurrentTickQueuesAtAll = tryNonCurrentTickQueuesAtAll; + } + + @Override + public boolean hasTasks() { + return ScheduledServerThreadTaskQueues.hasTasks(this.tryNonCurrentTickQueuesAtAll); + } + + @Override + public boolean hasTasks(TaskSpan span) { + return span == TaskSpan.YIELDING && this.hasTasks(); + } + + @Override + public boolean canHaveTasks(TaskSpan span) { + return span == TaskSpan.YIELDING; + } + + @Override + public @Nullable Runnable poll(BaseThread currentThread) { + return ScheduledServerThreadTaskQueues.poll((ServerThread) currentThread, this.tryNonCurrentTickQueuesAtAll); + } + + @Override + public @Nullable Runnable pollTiny(BaseThread currentThread) { + return null; + } + + @Override + public void add(Runnable task, TaskSpan span) { + throw new UnsupportedOperationException(); + } + + @Override + public void setTier(BaseTaskQueueTier tier) { + if (ScheduledServerThreadTaskQueues.tier == null) { + ScheduledServerThreadTaskQueues.tier = tier; + } + } + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/AbstractYieldingThread.java b/src/main/java/org/galemc/gale/executor/thread/AbstractYieldingThread.java new file mode 100644 index 0000000000000000000000000000000000000000..bb2055d8da984671762d9a5f8c4dc12f9353ceeb --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/AbstractYieldingThread.java @@ -0,0 +1,43 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.thread; + +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.lock.YieldingLock; +import org.jetbrains.annotations.Nullable; + +import java.util.function.BooleanSupplier; + +/** + * An interface for threads that can yield to other tasks, for example upon encountering a {@link YieldingLock}, + * in lieu of blocking. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public interface AbstractYieldingThread extends SignallableThread { + + + void yieldUntil(@Nullable BooleanSupplier stopCondition, @Nullable YieldingLock yieldingLock); + + + void runTasksUntil(@Nullable BooleanSupplier stopCondition, @Nullable YieldingLock yieldingLock); + + + @AnyThreadSafe + @YieldFree + static @Nullable AbstractYieldingThread currentYieldingThread() { + return Thread.currentThread() instanceof AbstractYieldingThread yieldingThread ? yieldingThread : null; + } + + /** + * @return Whether the current thread is a {@link AbstractYieldingThread}. + */ + @SuppressWarnings("unused") + @AnyThreadSafe + @YieldFree + static boolean isYieldingThread() { + return Thread.currentThread() instanceof AbstractYieldingThread; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/AssistThread.java b/src/main/java/org/galemc/gale/executor/thread/AssistThread.java new file mode 100644 index 0000000000000000000000000000000000000000..a5605765f6be0b75e5df5613e8b393b64f88fc3c --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/AssistThread.java @@ -0,0 +1,78 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.thread; + +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.thread.BaseThreadOnly; +import org.galemc.gale.executor.annotation.thread.ThisThreadOnly; +import org.galemc.gale.executor.thread.pool.BaseThreadPool; +import org.jetbrains.annotations.Nullable; + +/** + * A thread created by the {@link BaseThreadPool}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public class AssistThread extends BaseThread { + + /** + * The maximum yield depth. While an {@link AssistThread} has a yield depth equal to or greater than this value, + * it can not start more potentially yielding tasks. + */ + public static final int MAXIMUM_YIELD_DEPTH = Integer.getInteger("gale.yield.depth.max", 100); + + /** + * The index of this thread, as needed as an argument to + * {@link BaseThreadPool#getThreadByAssistIndex(int)}. + */ + public final int assistThreadIndex; + + /** + * Must only be called from {@link BaseThreadPool#addAssistThread}. + */ + public AssistThread(int assistThreadIndex) { + super(AssistThread::getCurrentAssistThreadAndRunForever, "Assist Thread " + assistThreadIndex, assistThreadIndex + 1, MAXIMUM_YIELD_DEPTH); + this.assistThreadIndex = assistThreadIndex; + } + + /** + * Causes this thread to loop forever, always attempting to find a task to do, and if none is found, + * registering itself with the places where a relevant task may be added in order to be signalled when + * one is actually added. + */ + @ThisThreadOnly + protected void runForever() { + this.runTasksUntil(() -> false, null); + } + + /** + * @return The current thread if it is a {@link AssistThread}, or null otherwise. + */ + @SuppressWarnings("unused") + @AnyThreadSafe + @YieldFree + public static @Nullable AssistThread currentAssistThread() { + return Thread.currentThread() instanceof AssistThread assistThread ? assistThread : null; + } + + /** + * @return Whether the current thread is a {@link AssistThread}. + */ + @SuppressWarnings("unused") + @AnyThreadSafe + @YieldFree + public static boolean isAssistThread() { + return Thread.currentThread() instanceof AssistThread; + } + + /** + * A method that simply acquires the {@link AssistThread} that is the current thread, and calls + * {@link #runForever()} on it. + */ + @BaseThreadOnly + protected static void getCurrentAssistThreadAndRunForever() { + ((AssistThread) Thread.currentThread()).runForever(); + } + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/BaseThread.java b/src/main/java/org/galemc/gale/executor/thread/BaseThread.java new file mode 100644 index 0000000000000000000000000000000000000000..1024c45be3d867af6563d4f6984e716cc0bc12d2 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/BaseThread.java @@ -0,0 +1,682 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.thread; + +import io.papermc.paper.util.TickThread; +import net.minecraft.server.MinecraftServer; +import org.galemc.gale.executor.TaskSpan; +import org.galemc.gale.executor.annotation.Access; +import org.galemc.gale.executor.annotation.PotentiallyYielding; +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.Guarded; +import org.galemc.gale.executor.annotation.PotentiallyBlocking; +import org.galemc.gale.executor.annotation.thread.ThisThreadOnly; +import org.galemc.gale.executor.annotation.YieldFree; +import org.galemc.gale.executor.lock.YieldingLock; +import org.galemc.gale.executor.queue.AbstractTaskQueue; +import org.galemc.gale.executor.queue.BaseTaskQueueTier; +import org.galemc.gale.executor.thread.pool.*; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.BooleanSupplier; + +/** + * An abstract base class implementing {@link AbstractYieldingThread}, + * that provides implementation that is common between + * {@link TickThread} and {@link AssistThread}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public abstract class BaseThread extends Thread implements AbstractYieldingThread { + + /** + * The minimum time to wait as the {@link MinecraftServer#serverThread} when performing a timed wait. + * Given in nanoseconds. + * If a timed wait with a lower time is attempted, the wait is not performed at all. + */ + public static final long SERVER_THREAD_WAIT_NANOS_MINIMUM = 10_000; + + /** + * The time to wait as the {@link MinecraftServer#serverThread} during the oversleep phase, if + * there may be delayed tasks. + * Given in nanoseconds. + */ + public static final long SERVER_THREAD_WAIT_NANOS_DURING_OVERSLEEP_WITH_DELAYED_TASKS = 50_000; + + /** + * The index of this thread, as needed as an argument to + * {@link BaseThreadPool#getThreadByBaseIndex(int)}. + */ + public final int baseThreadIndex; + + /** + * The maximum yield depth for this thread, + * which equals 1 for a {@link ServerThread} + * and {@link AssistThread#MAXIMUM_YIELD_DEPTH} for an {@link AssistThread}. + */ + public final int maximumYieldDepth; + + /** + * The current yield depth of this thread. + */ + @AnyThreadSafe(Access.READ) @ThisThreadOnly(Access.WRITE) + public volatile int yieldDepth = 0; + + /** + * Whether this thread can currently start yielding tasks with respect to being restricted + * due to {@link #yieldDepth} being at least {@link #maximumYieldDepth}. + *
    + * This is updated after {@link #yieldDepth} is updated. + */ + @AnyThreadSafe(Access.READ) @ThisThreadOnly(Access.WRITE) + public volatile boolean canStartYieldingTasks = true; + + /** + * The highest {@link BaseTaskQueueTier} of any task on the yielding execution stack of this thread, + * or null if there is no task being executed on this thread. + */ + @AnyThreadSafe(Access.READ) @ThisThreadOnly(Access.WRITE) + public volatile @Nullable BaseTaskQueueTier highestTierOfTaskOnStack; + + /** + * The {@link BaseTaskQueueTier} that the last non-null return value of {@link #pollTask} was polled from, + * or null if {@link #pollTask} has never been called yet. + */ + @ThisThreadOnly + private @Nullable BaseTaskQueueTier lastPolledTaskTier; + + /** + * The lock to guard this thread's sleeping and waking actions. + */ + private final Lock waitLock = new ReentrantLock(); + + /** + * The condition to wait for a signal, when this thread has to wait for something to do. + */ + private final Condition waitCondition = waitLock.newCondition(); + + /** + * Whether this thread is currently not working on the content of a task, but instead + * attempting to poll a next task to do, checking whether it can accept tasks at all, or + * attempting to acquire a {@link YieldingLock}, or waiting (although the fact that this value is true during + * waiting is irrelevant, because at such a time, {@link #isWaiting} will be true, and this value will no longer + * have any effect due to the implementation of {@link #signal}). + *
    + * This value is used to determine whether to set {@link #skipNextWait} when {@link #signal} is called + * and {@link #isWaiting} is false. + */ + @AnyThreadSafe(Access.READ) @ThisThreadOnly(Access.WRITE) + private volatile boolean isPollingTaskOrCheckingStopCondition = true; + + /** + * Whether this thread should not start waiting for something to do the next time no task could be polled, + * but instead try polling a task again. + */ + @AnyThreadSafe + public volatile boolean skipNextWait = false; + + /** + * Whether this thread is currently waiting for something to do. + *
    + * This is set to true at some point before actually starting to wait in a blocking fashion, + * and set to false at some point after no longer waiting in a blocking fashion. So, at some point, + * this value may be true while the thread is not blocked yet, or anymore. + * Even more so, extra checks for whether the thread should block will be performed in between + * the moment this value is set to true and the moment the thread potentially blocks. This means that if the + * checks fail, this value may be set to true and then false again, without actually ever blocking. + */ + @AnyThreadSafe(Access.READ) @ThisThreadOnly(Access.WRITE) + @Guarded(value = "#waitLock", fieldAccess = Access.WRITE) + public volatile boolean isWaiting = false; + + /** + * Whether {@link #isWaiting} is irrelevant because this thread has already + * been signalled via {@link #signal} to wake up. + */ + @AnyThreadSafe(Access.READ) @ThisThreadOnly(Access.WRITE) + @Guarded(value = "#waitLock", fieldAccess = Access.WRITE) + public volatile boolean mayBeStillWaitingButHasBeenSignalled = false; + + /** + * The {@link YieldingLock} that this thread is waiting for, + * or null if this thread is not waiting for a {@link YieldingLock}. + * This value only has meaning while {@link #isWaiting} is true. + */ + @AnyThreadSafe(Access.READ) @ThisThreadOnly(Access.WRITE) + @Guarded(value = "#waitLock", fieldAccess = Access.WRITE) + public volatile @Nullable YieldingLock lockWaitingFor = null; + + /** + * A special flag, used after changing {@link #isWaiting}, when the lock must be temporarily released to + * call {@link BaseThreadActivation#callForUpdate()} (to avoid deadlocks in {@link #signal} calls), + * and we wish the pool to regard this thread as waiting + * (which it will, because {@link #isWaiting} will be true), but we must still + * know not to signal the underlying {@link #waitCondition}, but set {@link #skipNextWait} to true, + * when {@link #signal} is called at some point during the short release of {@link #waitLock}. + */ + public volatile boolean isNotActuallyWaitingYet = false; + + /** + * The last reason this thread was signalled before the current poll attempt, or null if the current + * poll attempt was not preceded by signalling (but by yielding for example). + */ + public volatile @Nullable SignalReason lastSignalReason = null; + + protected BaseThread(Runnable target, String name, int baseThreadIndex, int maximumYieldDepth) { + super(target, name); + this.baseThreadIndex = baseThreadIndex; + this.maximumYieldDepth = maximumYieldDepth; + } + + /** + * This method is based on {@link #signal}. + * + * @see #signal + */ + @SuppressWarnings("RedundantIfStatement") + public boolean isWaitingAndNeedsSignal() { + if (this.isWaiting) { + if (this.isNotActuallyWaitingYet) { + if (!this.skipNextWait) { + return true; + } + return false; + } + if (!this.mayBeStillWaitingButHasBeenSignalled) { + return true; + } + } else if (this.isPollingTaskOrCheckingStopCondition) { + if (!this.skipNextWait) { + return true; + } + } + return false; + } + + /** + * Yields to tasks: polls and executes tasks while possible and the stop condition is not met. + * The stop condition is met if {@code stopCondition} is not null and returns true, or alternatively, + * if {@code stopCondition} is null, and {@code yieldingLock} is successfully acquired. + * When no tasks can be polled, this thread will block, waiting for either a task that can be executed by this + * thread to become available, or for the {@code yieldingLock}, if given, to be released. + *
    + * Exactly one of {@code stopCondition} and {@code yieldingLock} must be non-null. + */ + @ThisThreadOnly + @PotentiallyYielding("this method is meant to yield") + public final void yieldUntil(@Nullable BooleanSupplier stopCondition, @Nullable YieldingLock yieldingLock) { + //noinspection NonAtomicOperationOnVolatileField + this.yieldDepth++; + if (this.canStartYieldingTasks && this.yieldDepth >= this.maximumYieldDepth) { + this.canStartYieldingTasks = false; + } + this.runTasksUntil(stopCondition, yieldingLock); + //noinspection NonAtomicOperationOnVolatileField + this.yieldDepth--; + if (!this.canStartYieldingTasks && this.yieldDepth < this.maximumYieldDepth) { + this.canStartYieldingTasks = true; + } + } + + /** + * This method will keep attempting to find a task to do, and execute it, and if none is found, start waiting + * until signalled by {@link BaseThreadPool} or by a {@link YieldingLock}. + * The loop is broken as soon as the stop condition becomes true, or the given lock is successfully acquired. + *
    + * The above is the same as {@link #yieldUntil}, except it may be called in situations that is not 'yielding', + * for instance the endless loop polling tasks performed by a n{@link AssistThread}. The difference with + * {@link #yieldUntil} is that this method does not increment or decrement things the yield depth of this thread. + * + * @see #yieldUntil + */ + @ThisThreadOnly + @PotentiallyYielding("may yield further if an executed task is potentially yielding") + public final 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; + } + } + + // If this is the original server thread, update isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking + if (this == MinecraftServer.serverThread) { + MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking = MinecraftServer.isInSpareTime && MinecraftServer.blockingCount == 0 && !MinecraftServer.SERVER.haveTime(); + } + + // Attempt to poll a task that can be started + Runnable task = this.pollTask(); + + // Run the task if found + if (task != null) { + + // If this is the server thread, potentially set nextTimeAssumeWeMayHaveDelayedTasks to true + if (this == MinecraftServer.serverThread && !MinecraftServer.nextTimeAssumeWeMayHaveDelayedTasks && AbstractTaskQueue.taskQueuesHaveTasks(BaseTaskQueueTier.SERVER.taskQueues)) { + MinecraftServer.nextTimeAssumeWeMayHaveDelayedTasks = true; + } + + // Update highestTierOfTaskOnStack and the thread priority + var highestTierBeforeTask = this.highestTierOfTaskOnStack; + var threadPriorityBeforeTask = this.getPriority(); + //noinspection DataFlowIssue + var newHighestTier = highestTierBeforeTask == null ? this.lastPolledTaskTier : highestTierBeforeTask.ordinal < this.lastPolledTaskTier.ordinal ? highestTierBeforeTask : this.lastPolledTaskTier; + //noinspection DataFlowIssue + var newThreadPriority = newHighestTier.threadPriority; + if (newHighestTier != highestTierBeforeTask) { + this.highestTierOfTaskOnStack = newHighestTier; + BaseThreadActivation.callForUpdate(); + if (threadPriorityBeforeTask != newThreadPriority) { + this.setPriority(newThreadPriority); + } + } + + this.isPollingTaskOrCheckingStopCondition = false; + task.run(); + + // If this is the server thread, execute some chunk tasks + if (this == MinecraftServer.serverThread) { + if (newHighestTier != BaseTaskQueueTier.SERVER) { + newHighestTier = BaseTaskQueueTier.SERVER; + this.highestTierOfTaskOnStack = newHighestTier; + BaseThreadActivation.callForUpdate(); + if (newThreadPriority != newHighestTier.threadPriority) { + newThreadPriority = newHighestTier.threadPriority; + this.setPriority(newThreadPriority); + } + } + MinecraftServer.SERVER.executeMidTickTasks(); // Paper - execute chunk tasks mid tick + } + + // Reset highestTierOfTaskOnStack and the thread priority + if (newHighestTier != highestTierBeforeTask) { + this.highestTierOfTaskOnStack = highestTierBeforeTask; + BaseThreadActivation.callForUpdate(); + if (threadPriorityBeforeTask != newThreadPriority) { + this.setPriority(threadPriorityBeforeTask); + } + } + + this.isPollingTaskOrCheckingStopCondition = true; + continue; + + } + + /* + If no task that can be started by this thread was found, wait for a task that we are allowed + to poll to become available (when that happens, the BaseThreadPool will signal this thread), + or for the given yielding lock to be released. This is the only time we should ever block inside + a potentially yielding procedure. + */ + this.waitUntilSignalled(yieldingLock); + + } + + this.isPollingTaskOrCheckingStopCondition = false; + + /* + If the thread was signalled for another reason than the lock, but we acquired the lock instead, + another thread should be signalled for that reason. + */ + SignalReason lastSignalReason = this.lastSignalReason; + if (lastSignalReason != null && yieldingLock != null && lastSignalReason != SignalReason.YIELDING_LOCK) { + BaseThreadActivation.callForUpdate(); + } + + } + + /** + * @see #pollTask() + */ + @ThisThreadOnly + @YieldFree + private @Nullable Runnable pollTaskFromTier(BaseTaskQueueTier tier, boolean tinyOnly) { + for (var queue : tier.taskQueues) { + Runnable task = tinyOnly ? queue.pollTiny(this) : queue.poll(this); + if (task != null) { + /* + Check if the tier has run out of tasks for a span, + in order to update BaseThreadActivation#thereMayBeTasks. + */ + for (int spanI = 0; spanI < TaskSpan.length; spanI++) { + TaskSpan span = TaskSpan.VALUES[spanI]; + if (queue.canHaveTasks(span)) { + int oldTasks = BaseThreadActivation.thereMayBeTasks[tier.ordinal][spanI].get(); + if (oldTasks > 0) { + if (!queue.hasTasks(span)) { + boolean tierHasNoTasksForSpan = true; + for (AbstractTaskQueue otherTierQueue : tier.taskQueues) { + // We already know there are no tasks in this queue + if (otherTierQueue == queue) { + continue; + } + if (otherTierQueue.hasTasks(span)) { + tierHasNoTasksForSpan = false; + break; + } + } + if (tierHasNoTasksForSpan) { + // Set thereMayBeTasks to false, but only if it did not change in the meantime + BaseThreadActivation.thereMayBeTasks[tier.ordinal][spanI].compareAndSet(oldTasks, 0); + } + } + } + } + } + this.lastPolledTaskTier = tier; + return task; + } + } + return null; + } + + /** + * Polls a task from any queue this thread can currently poll from, and returns it. + * Polling potentially yielding tasks is attempted before yield-free tasks. + * + * @return The task that was polled, or null if no task was found. + */ + @ThisThreadOnly + @YieldFree + private @Nullable Runnable pollTask() { + /* + * If this is a server thread, poll from SERVER, and poll tiny tasks from other tiers. + * Note that when polling on the ServerThread, we do not check whether we would be allowed to do so + * by the BaseThreadPool, as we consider keeping the ServerThread in the Thread.State.RUNNABLE state for + * as long as possible to be more important than the off-chance of for example starting a TINY ASYNC task + * on the server thread while no ASYNC tasks are allowed to be polled by other threads at the moment. + */ + if (this instanceof ServerThread) { + // Poll from the SERVER queues + Runnable task = this.pollTaskFromTier(BaseTaskQueueTier.SERVER, false); + if (task != null) { + return task; + } + // Poll tiny tasks from other tiers + for (var tier : BaseTaskQueueTier.VALUES_EXCEPT_SERVER) { + task = this.pollTaskFromTier(tier, true); + if (task != null) { + return task; + } + } + // We failed to poll any task + return null; + } + // If this is not a server thread, poll from all queues except SERVER + for (var tier : BaseTaskQueueTier.VALUES_EXCEPT_SERVER) { + /* + Make sure that we are allowed to poll from the tier, according to the presence of an excess number of + threads working on tasks from that tier during the last BaseThreadActivation#update call. + In the case this check's result is too optimistic, and a task is started when ideally it wouldn't have been, + then so be it - it is not terrible. Whenever this happens, enough threads will surely be allocated + by the BaseThreadPool for the task tier that is more in demand anyway, so it does not matter much. + In the case this check's result is too pessimistic, the polling fails and this thread will start to sleep, + but before doing this, will make a call to BaseThreadActivation#callForUpdate that re-activated this + thread if necessary, so no harm is done. + In the case this check causes this thread to go to sleep, the call to BaseThreadActivation#callForUpdate + while isWaiting is true will make sure the BaseThreadPool has the ability to correctly activate a + different thread (that is able to start tasks of a higher tier) if needed. + Here, we do not even make an exception for TINY tasks, since there may already be ongoing avoidable + context-switching due to excess threads that we can solve by letting this thread go to sleep. + */ + if (tier.ordinal < BaseThreadActivation.tierInExcessOrdinal) { + /* + Tasks of a certain tier may yield to tasks of the same or a higher + tier, and they may also yield to tiny tasks of a lower tier. + */ + var tierYieldingFrom = this.highestTierOfTaskOnStack; + Runnable task = this.pollTaskFromTier(tier, tierYieldingFrom != null && tier.ordinal > tierYieldingFrom.ordinal); + if (task != null) { + return task; + } + } + } + // We failed to poll any task + return null; + } + + /** + * Starts waiting on something to do. + * + * @param yieldingLock A {@link YieldingLock} to register with, or null if this thread is not waiting for + * a yielding lock. + */ + @ThisThreadOnly + @PotentiallyBlocking + private void waitUntilSignalled(@Nullable YieldingLock yieldingLock) { + + // Remember whether we registered to wait with the lock, to unregister later + // Register this thread with the lock if necessary + boolean registeredAsWaitingWithLock = false; + if (yieldingLock != null) { + // No point in registering if we're not going to wait anyway + if (!this.skipNextWait) { + yieldingLock.incrementWaitingThreads(); + registeredAsWaitingWithLock = true; + } + } + + /* + Remember whether we changed anything that requires a BaseThreadPool#update call + (after the last call to that method). + */ + boolean mustCallPoolUpdateAtEnd = false; + + /* + If we cannot acquire the lock, we can assume this thread is being signalled, + so there is no reason to start waiting. + */ + waitWithLock: if (this.waitLock.tryLock()) { + try { + + // If it was set that this thread should skip the wait in the meantime, skip it + if (this.skipNextWait) { + break waitWithLock; + } + + // Mark this thread as waiting + this.lockWaitingFor = yieldingLock; + this.mayBeStillWaitingButHasBeenSignalled = false; + this.isWaiting = true; + // But actually we are not waiting yet, signal has no effect yet during the next short lock release + this.isNotActuallyWaitingYet = true; + + } finally { + this.waitLock.unlock(); + } + + // Update the pool + BaseThreadActivation.callForUpdate(); + + /* + If we cannot acquire the lock, we can assume this thread is being signalled, + so there is no reason to start waiting. + */ + if (this.waitLock.tryLock()) { + try { + + // We passed the short lock release + this.isNotActuallyWaitingYet = false; + + // If it was set that this thread should skip the wait in the meantime, skip it + if (this.skipNextWait) { + this.isWaiting = false; + this.lockWaitingFor = null; + mustCallPoolUpdateAtEnd = true; + break waitWithLock; + } + + // 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) { + //noinspection ResultOfMethodCallIgnored + this.waitCondition.await(waitForNanos, TimeUnit.NANOSECONDS); + } + } + } + + /* + 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.waitCondition.await(); + } + + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + + // Unmark this thread as waiting + this.isWaiting = false; + this.lockWaitingFor = null; + mustCallPoolUpdateAtEnd = true; + + } finally { + this.waitLock.unlock(); + } + } + + } + + // Unregister this thread from the lock if necessary + if (registeredAsWaitingWithLock) { + yieldingLock.decrementWaitingThreads(); + } + + // Reset skipping the next wait + this.skipNextWait = false; + + // Update the pool if necessary + if (mustCallPoolUpdateAtEnd) { + BaseThreadActivation.callForUpdate(); + } + + } + + /** + * Signals this thread to wake up, or if it was not sleeping but attempting to poll a task: + * to not go to sleep the next time no task could be polled, and instead try polling a task again. + * + * @param reason The reason why this thread was signalled, or null if it is irrelevant (e.g. when the signal + * will never need to be repeated because there is only thread waiting for this specific event + * to happen). + * @return Whether this thread was sleeping before, and has woken up now, + * or whether {@link #skipNextWait} was set to true. + */ + @AnyThreadSafe + @YieldFree + public final boolean signal(@Nullable SignalReason reason) { + //noinspection StatementWithEmptyBody + while (!this.waitLock.tryLock()); // TODO Gale use a wait-free system here by using a sort of leave-a-message-at-the-door Atomic class system + try { + if (this.isWaiting) { + if (this.isNotActuallyWaitingYet) { + if (!this.skipNextWait) { + this.lastSignalReason = reason; + this.skipNextWait = true; + return true; + } + return false; + } + if (!this.mayBeStillWaitingButHasBeenSignalled) { + this.lastSignalReason = reason; + this.mayBeStillWaitingButHasBeenSignalled = true; + this.waitCondition.signal(); + return true; + } + } else if (this.isPollingTaskOrCheckingStopCondition) { + if (!this.skipNextWait) { + this.lastSignalReason = reason; + this.skipNextWait = true; + return true; + } + } + return false; + } finally { + this.waitLock.unlock(); + } + } + + /** + * @return The current thread if it is a {@link BaseThread}, or null otherwise. + */ + @SuppressWarnings("unused") + @AnyThreadSafe + @YieldFree + public static @Nullable BaseThread currentBaseThread() { + return Thread.currentThread() instanceof BaseThread baseThread ? baseThread : null; + } + + /** + * @return Whether the current thread is a {@link BaseThread}. + */ + @SuppressWarnings("unused") + @AnyThreadSafe + @YieldFree + public static boolean isBaseThread() { + return Thread.currentThread() instanceof BaseThread; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/OriginalServerThread.java b/src/main/java/org/galemc/gale/executor/thread/OriginalServerThread.java new file mode 100644 index 0000000000000000000000000000000000000000..ced372b40e8b3a5c43dabf5bb547a71e3c713d2f --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/OriginalServerThread.java @@ -0,0 +1,20 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.thread; + +import net.minecraft.server.MinecraftServer; +import org.spigotmc.WatchdogThread; + +/** + * A type that is unique to {@link MinecraftServer#serverThread}, + * to distinguish it from {@link WatchdogThread#instance}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public final class OriginalServerThread extends ServerThread { + + public OriginalServerThread(final Runnable run, final String name) { + super(run, name); + } + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/ServerThread.java b/src/main/java/org/galemc/gale/executor/thread/ServerThread.java new file mode 100644 index 0000000000000000000000000000000000000000..7d58d995d8e74cd5f51f85f123166bf884deed92 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/ServerThread.java @@ -0,0 +1,51 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.thread; + +import io.papermc.paper.util.TickThread; +import net.minecraft.server.MinecraftServer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.spigotmc.WatchdogThread; + +/** + * A {@link TickThread} that provides an implementation for {@link BaseThread}, + * that is shared between the {@link MinecraftServer#serverThread} and {@link WatchdogThread#instance}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public class ServerThread extends TickThread { + + protected ServerThread(final String name) { + super(name); + } + + protected ServerThread(final Runnable run, final String name) { + super(run, name); + } + + /** + * This method must not be called while {@link MinecraftServer#isConstructed} is false. + * + * @return The global {@link ServerThread} instance, which is either + * {@link MinecraftServer#serverThread}, or {@link WatchdogThread#instance} while the server is shutting + * down and the {@link WatchdogThread} was responsible. + */ + public static @NotNull ServerThread getInstance() { + if (MinecraftServer.SERVER.hasStopped) { + if (MinecraftServer.SERVER.shutdownThread == WatchdogThread.instance) { + return WatchdogThread.instance; + } + } + return MinecraftServer.serverThread; + } + + /** + * @return The same value as {@link #getInstance()} if {@link MinecraftServer#isConstructed} is true, + * or null otherwise. + */ + public static @Nullable ServerThread getInstanceIfConstructed() { + return MinecraftServer.isConstructed ? getInstance() : null; + } + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/SignalReason.java b/src/main/java/org/galemc/gale/executor/thread/SignalReason.java new file mode 100644 index 0000000000000000000000000000000000000000..436b0a8249290d833472da58ec01f9690be2fb95 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/SignalReason.java @@ -0,0 +1,23 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.thread; + +import org.galemc.gale.executor.lock.YieldingLock; + +/** + * A reason of a call to {@link SignallableThread#signal}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public enum SignalReason { + + /** + * A task that the signalled thread could poll and start is available. + */ + TASK, + /** + * The {@link YieldingLock} that the signalled thread was waiting for was released. + */ + YIELDING_LOCK + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/SignallableThread.java b/src/main/java/org/galemc/gale/executor/thread/SignallableThread.java new file mode 100644 index 0000000000000000000000000000000000000000..a73aafc64dc60b57e2e5a91565e1aff612da6703 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/SignallableThread.java @@ -0,0 +1,31 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.thread; + +import org.galemc.gale.executor.annotation.thread.AnyThreadSafe; +import org.galemc.gale.executor.annotation.YieldFree; +import org.jetbrains.annotations.Nullable; + +/** + * An interface for threads that can wait (either by blocking or yielding) for events, and be signalled when + * circumstances may have changed. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public interface SignallableThread { + + /** + * Signals this thread to wake up, or if it was not sleeping but attempting to poll a task: + * to not go to sleep the next time no task could be polled, and instead try polling a task again. + * + * @param reason The reason why this thread was signalled, or null if it is irrelevant (e.g. when the signal + * will never need to be repeated because there is only thread waiting for this specific event + * to happen). + * @return Whether this thread was sleeping before, and had not been signalled to wake up before, + * but has or will be woken up due to this signal. + */ + @AnyThreadSafe + @YieldFree + boolean signal(@Nullable SignalReason reason); + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/deferral/ServerThreadDeferral.java b/src/main/java/org/galemc/gale/executor/thread/deferral/ServerThreadDeferral.java new file mode 100644 index 0000000000000000000000000000000000000000..57f672f9f81aeb6ce0ef44fa47db80602b03a5d4 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/deferral/ServerThreadDeferral.java @@ -0,0 +1,151 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.thread.deferral; + +import io.papermc.paper.util.TickThread; +import org.galemc.gale.concurrent.UnterminableExecutorService; +import org.galemc.gale.executor.TaskSpan; +import org.galemc.gale.executor.queue.BaseTaskQueues; +import org.galemc.gale.executor.thread.AbstractYieldingThread; +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.ServerThread; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.function.Supplier; + +/** + * This class provides functionality to allow any thread, + * including but not limited to a {@link BaseThread}, + * to defer blocks of code to a {@link ServerThread}, and wait for its completion. + *
    + * Using deferral from a {@link TickThread} that is not the correct thread already is highly discouraged + * because yielding from a {@link TickThread} should be avoided whenever possible. + * + * @see TickThreadDeferral + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@SuppressWarnings("unused") +public final class ServerThreadDeferral { + + private ServerThreadDeferral() {} + + /** + * @see #defer(Supplier, TaskSpan) + */ + public static void defer(Runnable task, TaskSpan span) { + deferInternal(task, null, span); + } + + /** + * Defers the given {@code task} to a {@link ServerThread}, and yields until it has finished. + * If this thread is a {@link ServerThread}, the task will be executed right away. + *
    + * The task itself must be non-blocking and may be potentially yielding, but keeping the task yield-free is + * highly preferred because during yielding from a {@link ServerThread}, most other tasks that must be + * executed on a {@link ServerThread} cannot be run. + *
    + * On an {@link AbstractYieldingThread}, this method yields until the task is completed. + * Like any potentially yielding method, while technically possible to call from any thread, this method should + * generally only be called from a yielding thread, because on any other thread, the thread will block until + * the given task has been completed by the main thread. + *
    + * If this thread is already an appropriate thread to run the task on, the task is performed on this thread. + * + * @param task The task to run. + * @param span The {@link TaskSpan} of the task. + */ + public static T defer(Supplier task, TaskSpan span) { + return deferInternal(null, task, span); + } + + /** + * Common implementation for {@link #defer(Runnable, TaskSpan)} and {@link #defer(Supplier, TaskSpan)}. + * Exactly one of {@code runnable} or {@code supplier} must be non-null. + */ + private static T deferInternal(@Nullable Runnable runnable, @Nullable Supplier supplier, TaskSpan span) { + // Check if we are the right thread + if (TickThread.isTickThread()) { + if (runnable == null) { + //noinspection ConstantConditions + return supplier.get(); + } + runnable.run(); + return null; + } + // Otherwise, schedule the task and wait for it to complete + CompletableFuture future = new CompletableFuture<>(); + AbstractYieldingThread yieldingThread = AbstractYieldingThread.currentYieldingThread(); + if (yieldingThread != null) { + // Yield until the task completes + BaseTaskQueues.deferredToServerThread.add(() -> { + if (runnable == null) { + //noinspection ConstantConditions + future.complete(supplier.get()); + } else { + runnable.run(); + future.complete(null); + } + yieldingThread.signal(null); + }, span); + yieldingThread.yieldUntil(future::isDone, null); + return future.getNow(null); + } else { + // Block until the task completes + BaseTaskQueues.deferredToServerThread.add(() -> { + if (runnable == null) { + //noinspection ConstantConditions + future.complete(supplier.get()); + } else { + runnable.run(); + future.complete(null); + } + }, span); + return future.join(); + } + } + + /** + * An executor for deferring {@link TaskSpan#YIELDING} tasks to the main thread, + * where {@link Executor#execute} calls {@link #defer}. + */ + public static final ExecutorService yieldingExecutor = new UnterminableExecutorService() { + + @Override + public void execute(@NotNull Runnable runnable) { + defer(runnable, TaskSpan.YIELDING); + } + + }; + + /** + * An executor for deferring {@link TaskSpan#FREE} tasks to the main thread, + * where {@link Executor#execute} calls {@link #defer}. + */ + public static final ExecutorService freeExecutor = new UnterminableExecutorService() { + + @Override + public void execute(@NotNull Runnable runnable) { + defer(runnable, TaskSpan.FREE); + } + + }; + + /** + * An executor for deferring {@link TaskSpan#TINY} tasks to the main thread, + * where {@link Executor#execute} calls {@link #defer}. + */ + public static final ExecutorService tinyExecutor = new UnterminableExecutorService() { + + @Override + public void execute(@NotNull Runnable runnable) { + defer(runnable, TaskSpan.TINY); + } + + }; + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/deferral/TickThreadDeferral.java b/src/main/java/org/galemc/gale/executor/thread/deferral/TickThreadDeferral.java new file mode 100644 index 0000000000000000000000000000000000000000..77fe10e51b00115da520cfc211bf84badbc027be --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/deferral/TickThreadDeferral.java @@ -0,0 +1,159 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.thread.deferral; + +import io.papermc.paper.util.TickThread; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import org.galemc.gale.concurrent.UnterminableExecutorService; +import org.galemc.gale.executor.TaskSpan; +import org.galemc.gale.executor.thread.AbstractYieldingThread; +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.queue.BaseTaskQueueTier; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.function.Supplier; + +/** + * This class provides functionality to allow any thread, + * including but not limited to a {@link BaseThread}, + * to defer blocks of code to a {@link TickThread}, and wait for its completion. + * In other words, instead of the typical paradigm where a block of code is executed + * while a lock is held by the thread, we do not acquire a lock, but instead schedule the code + * to be run on a thread responsible for the specific aspects of the code + * (thereby avoiding deadlocks caused by the acquisition of multiple locks in various orders, + * and avoiding collisions between parts of code that can not run concurrently, + * which occur especially easy in parts of code that may have to call callbacks of which + * we can only make limited assumptions) and wait for that to finish. + *
    + * This has a number of advantages. + * When we require running code that checks whether it is being run on an appropriate {@link TickThread}, + * we can run it this way. Since these parts of code are always performed on a {@link TickThread} + * regardless of the thread requesting them to be run, there is no chance of deadlock occurring + * from two different locks being desired in a different order on two of the original threads + * (in fact, if the normally guarded blocks of code are always run exclusively to each other + * when deferred this way, we do not need locks at all). + *
    + * When deferring from an {@link AbstractYieldingThread}, + * we yield to other tasks until the deferred block of code has finished. + * When deferring from another type of thread, the thread is blocked. + *
    + * Using deferral from a {@link TickThread} that is not the correct thread already is highly discouraged + * because yielding from a {@link TickThread} should be avoided whenever possible. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@SuppressWarnings("unused") +public final class TickThreadDeferral { + + private TickThreadDeferral() {} + + /** + * This may be useful in the future. See the documentation of {@link BaseTaskQueueTier#SERVER}. + * + * @see #defer(Runnable, TaskSpan) + */ + public static void defer(final ServerLevel world, final int chunkX, final int chunkZ, Runnable task, TaskSpan span) { + defer(task, span); + } + + /** + * This may be useful in the future. See the documentation of {@link BaseTaskQueueTier#SERVER}. + * + * @see #defer(Supplier, TaskSpan) + */ + public static T defer(final ServerLevel world, final int chunkX, final int chunkZ, Supplier task, TaskSpan span) { + return defer(task, span); + } + + /** + * This may be useful in the future. See the documentation of {@link BaseTaskQueueTier#SERVER}. + * + * @see #defer(Runnable, TaskSpan) + */ + public static void defer(final Entity entity, Runnable task, TaskSpan span) { + defer(task, span); + } + + /** + * This may be useful in the future. See the documentation of {@link BaseTaskQueueTier#SERVER}. + * + * @see #defer(Supplier, TaskSpan) + */ + public static T defer(final Entity entity, Supplier task, TaskSpan span) { + return defer(task, span); + } + + /** + * @see #defer(Supplier, TaskSpan) + */ + public static void defer(Runnable task, TaskSpan span) { + // Current implementation uses ServerThreadDeferral + ServerThreadDeferral.defer(task, span); + } + + /** + * Defers the given {@code task} to any {@link TickThread}, and yields until it has finished. + * If this thread is a {@link TickThread}, the task will be executed right away. + *
    + * The task itself must be non-blocking and may be potentially yielding, but keeping the task yield-free is + * highly preferred because during yielding from a {@link TickThread}, other tasks that must be executed on that + * thread cannot be run. + *
    + * On a {@link AbstractYieldingThread}, this method yields until the task is completed. + * Like any potentially yielding method, while technically possible to call from any thread, this method should + * generally only be called from a yielding thread, because on any other thread, the thread will block until + * the given task has been completed by the main thread. + *
    + * If this thread is already an appropriate thread to run the task on, the task is performed on this thread. + * + * @param task The task to run. + * @param span The {@link TaskSpan} of the task. + */ + public static T defer(Supplier task, TaskSpan span) { + // Current implementation uses ServerThreadDeferral + return ServerThreadDeferral.defer(task, span); + } + + /** + * An executor for deferring {@link TaskSpan#YIELDING} tasks to the main thread, + * where {@link Executor#execute} calls {@link #defer}. + */ + public static final ExecutorService yieldingExecutor = new UnterminableExecutorService() { + + @Override + public void execute(@NotNull Runnable runnable) { + defer(runnable, TaskSpan.YIELDING); + } + + }; + + /** + * An executor for deferring {@link TaskSpan#FREE} tasks to the main thread, + * where {@link Executor#execute} calls {@link #defer}. + */ + public static final ExecutorService freeExecutor = new UnterminableExecutorService() { + + @Override + public void execute(@NotNull Runnable runnable) { + defer(runnable, TaskSpan.FREE); + } + + }; + + /** + * An executor for deferring {@link TaskSpan#TINY} tasks to the main thread, + * where {@link Executor#execute} calls {@link #defer}. + */ + public static final ExecutorService tinyExecutor = new UnterminableExecutorService() { + + @Override + public void execute(@NotNull Runnable runnable) { + defer(runnable, TaskSpan.TINY); + } + + }; + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/pool/BaseThreadActivation.java b/src/main/java/org/galemc/gale/executor/thread/pool/BaseThreadActivation.java new file mode 100644 index 0000000000000000000000000000000000000000..4c5600945e57525a21fa0a39c6b627e9794afad7 --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/pool/BaseThreadActivation.java @@ -0,0 +1,506 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.thread.pool; + +import net.minecraft.server.MinecraftServer; +import org.galemc.gale.executor.TaskSpan; +import org.galemc.gale.executor.lock.YieldingLock; +import org.galemc.gale.executor.queue.BaseTaskQueueTier; +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.SignalReason; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +/** + * A class providing the static functionality needed to activate more threads in the {@link BaseThreadPool} + * when needed. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public final class BaseThreadActivation { + + private BaseThreadActivation() {} + + /** + * The delay in nanoseconds that is applied to {@link System#nanoTime()} + * when computing {@link #nextAllowedFrequentSignalNewTasksTime}. + */ + public static final long FREQUENT_SIGNAL_NEW_TASKS_INTERVAL = 100_000; + + /** + * The last time {@link #newTaskWasAdded}'s content was actually run. + * This value is useful to limit the number of runs of the method by potential frequent callers, + * such as the chunk task executors. + */ + private static final AtomicLong nextAllowedFrequentSignalNewTasksTime = new AtomicLong(System.nanoTime() - 1L); + + /** + * @see #update() + */ + static final AtomicBoolean isUpdateOngoing = new AtomicBoolean(); + + /** + * @see #update() + */ + private static final AtomicInteger newUpdateCallsReceived = new AtomicInteger(); + + /** + * A re-usable array for use inside {@link #update()}. + */ + private static final int[] numberOfThreadsActiveForTier = new int[BaseTaskQueueTier.length]; + + /** + * A re-usable array for use inside {@link #update()}. + */ + @SuppressWarnings("unchecked") + private static final List[] threadsWaitingForLockForTier = new List[BaseTaskQueueTier.length]; + static { + for (int tierI = 0; tierI < BaseTaskQueueTier.length; tierI++) { + threadsWaitingForLockForTier[tierI] = new ArrayList<>(); + } + } + + /** + * A re-usable array for use inside {@link #update()}. + */ + private static final int[] numberOfThreadsActiveForLowerThanTier = new int[BaseTaskQueueTier.length]; + + /** + * A re-usable array for use inside {@link #update()}. + */ + private static final int[] numberOfThreadsActiveForHigherThanTier = new int[BaseTaskQueueTier.length]; + + /** + * A re-usable array for use inside {@link #update()}. + */ + private static final int[] numberOfThreadsIntendedToBeActiveForTier = new int[BaseTaskQueueTier.length]; + + /** + * An array indicating per {@link BaseTaskQueueTier} (indexed by their {@link BaseTaskQueueTier#ordinal}) + * per {@link TaskSpan} (indexed by their {@link TaskSpan#ordinal}) whether there may be tasks + * for that tier and span, indicated by whether the value is positive (indicating true) or 0 (indicating false). + * It is always incremented before calling {@link #update()} due to new tasks being added. + * If it is 0, it is certain that either there are no queued task for the tier, or + * a task has just been added to the queue and this value has not yet been set to true, but will be due + * to a {@link #newTaskWasAdded} call, which is then followed by a {@link #callForUpdate()} call. + */ + public static final AtomicInteger[][] thereMayBeTasks = new AtomicInteger[BaseTaskQueueTier.length][TaskSpan.length]; + static { + for (int tierI = 0; tierI < BaseTaskQueueTier.length; tierI++) { + for (int spanI = 0; spanI < TaskSpan.length; spanI++) { + thereMayBeTasks[tierI][spanI] = new AtomicInteger(); + } + } + } + + /** + * The {@link BaseTaskQueueTier#ordinal} of the highest tier (which means the lowest + * {@link BaseTaskQueueTier#ordinal}) for which the number of present threads + * have been determined by the last call to {@link #update()} to be in excess. This value is + * {@link BaseTaskQueueTier#length} when no threads are in excess. + */ + public static volatile int tierInExcessOrdinal = BaseTaskQueueTier.length; + + private static long updateNextAllowedFrequentSignalNewTasksTime(long value) { + long newValue = System.nanoTime() + FREQUENT_SIGNAL_NEW_TASKS_INTERVAL; + return newValue - value >= 0 ? newValue : value; + } + + /** + * @see #newTaskWasAdded(BaseTaskQueueTier, TaskSpan, boolean) + */ + public static void newTaskWasAdded(BaseTaskQueueTier tier, TaskSpan span) { + newTaskWasAdded(tier, span, false); + } + + /** + * This method is to be called when a new task has become available to be polled. + * The task must already have been added to the data structure that a thread would poll from, + * in a way that is visible to any thread (for example by adding it to a concurrent data structure). + * Otherwise, the resulting attempt at activating threads would not be able to observe these new tasks yet. + *
    + * When a task is added that is not important enough to warrant doing a full {@link #update}, + * calling this method may be skipped. + *
    + * Additionally, this method may be called when no new task has been added, but there is a suspicion of new tasks + * existing for which no {@link #update} was called. A concrete example of this is when a thread is activated + * due to tasks it can poll being available, but then upon activation, acquiring a {@link YieldingLock} it was + * waiting for instead. + */ + public static void newTaskWasAdded(BaseTaskQueueTier tier, TaskSpan span, boolean onlyIfLastTimeIsTooLongAgo) { + + if (thereMayBeTasks[tier.ordinal][span.ordinal].getAndIncrement() == 0) { + // Always call update() if we just set the thereMayBeTasks value to true + onlyIfLastTimeIsTooLongAgo = false; + } + + // Check and update nextAllowedFrequentSignalNewTasksTime + if (!onlyIfLastTimeIsTooLongAgo || System.nanoTime() - nextAllowedFrequentSignalNewTasksTime.get() >= 0) { + nextAllowedFrequentSignalNewTasksTime.updateAndGet(BaseThreadActivation::updateNextAllowedFrequentSignalNewTasksTime); + // Update + callForUpdate(); + } else { + // Do not start an update, but do increment the received calls + newUpdateCallsReceived.incrementAndGet(); + } + + } + + /** + * This method is to be called when a {@link YieldingLock} has been released. + * The lock must already have been unlocked. Otherwise, the resulting attempt at activating + * threads would not be able to observe the lock being released yet. + */ + public static void yieldingLockWithWaitingThreadsWasUnlocked() { + // Update + callForUpdate(); + } + + /** + * Either starts an {@link #update()}, or lets another thread that is already doing an update know + * that it will have to do another one. + *
    + * Only one thread can be performing an update at a time. + * If a second thread calls this method while an update is ongoing (signified by {@link #isUpdateOngoing}), + * the thread performing an update will perform another update after finishing the current one, due to the + * second thread incrementing {@link #newUpdateCallsReceived}. + *
    + * After a thread property (or another property that is used in a similar way) + * that is used within {@link #update()} is changed, this method must be called. + * This currently equates to the following values: + *
      + *
    • {@link BaseThread#highestTierOfTaskOnStack}
    • + *
    • + * {@link BaseThread#isWaiting} and {@link BaseThread#lockWaitingFor}, + * which are always updated in tandem, and {@link BaseThread#isNotActuallyWaitingYet} and + * {@link BaseThread#skipNextWait}, which are set at similar times as {@link BaseThread#isWaiting}. + *
    • + *
    • + * {@link BaseThread#yieldDepth} and {@link BaseThread#canStartYieldingTasks}, + * which are always updated in tandem. + *
    • + *
    + * This specifically does not include: + *
      + *
    • + * The following values that are only used + * in the meta-handling of {@link #update()}, not in the activation of threads: + *
        + *
      • {@link #newUpdateCallsReceived}
      • + *
      • {@link #isUpdateOngoing}
      • + *
      + *
    • + *
    • + * The following values that are never changed outside of {@link #update()}: + *
        + *
      • {@link #numberOfThreadsActiveForTier}
      • + *
      • {@link #threadsWaitingForLockForTier}
      • + *
      • {@link #numberOfThreadsActiveForLowerThanTier}
      • + *
      • {@link #numberOfThreadsActiveForHigherThanTier}
      • + *
      • {@link #numberOfThreadsIntendedToBeActiveForTier}
      • + *
      + *
    • + *
    • + * {@link #thereMayBeTasks}, which is only set to 0 outside of {@link #update()} + * (specifically, in {@link BaseThread}), which will only prevent the {@link #update()} call from + * exploring the existence of tasks for a specific {@link BaseTaskQueueTier} and {@link TaskSpan} when + * there are in fact no such tasks, thereby not causing any reason to do another update. + *
    • + *
    + */ + public static void callForUpdate() { + // Make sure the updating thread repeats (must be set before evaluating isUpdateOngoing) + newUpdateCallsReceived.incrementAndGet(); + // Start the update ourselves if not ongoing + if (!isUpdateOngoing.getAndSet(true)) { + // Perform an update + do { + try { + update(); + } finally { + isUpdateOngoing.set(false); + } + /* + If newUpdateCallsReceived is positive, it was increased between it being set to 0 and + isUpdateOngoing being set to false, so we must repeat. + */ + } while (newUpdateCallsReceived.get() > 0); + } + } + + /** + * Activates threads as necessary, and computes whether threads must de-activate themselves when they can. + *
    + * This method is called from {@link #callForUpdate()} if necessary. + */ + static void update() { + MinecraftServer.THREAD_DEBUG_LOGGER.ifPresent(it -> it.info("update")); + boolean madeChangesInLastIteration = false; + int numberOfUpdateCallsAtStartOfLastIteration = -1; + boolean isFirstIteration = true; + /* + Keep updating while necessary (while marked to repeat by another call, + or while this update itself made some change in the previous iteration, + to be sure we only stop when we found no more changes to make). + */ + updateWhileNecessary: + while (true) { + MinecraftServer.THREAD_DEBUG_LOGGER.ifPresent(it -> it.info("iteration of update")); + + // Break the loop if needed + if (isFirstIteration) { + // Always run an iteration if this is the first one + isFirstIteration = false; + numberOfUpdateCallsAtStartOfLastIteration = newUpdateCallsReceived.decrementAndGet(); + } else { + if (madeChangesInLastIteration) { + /* + If we made changes in the last iteration, + we can quit only if no more update calls have been received at all. + */ + int oldNewUpdateCallsReceived = newUpdateCallsReceived.getAndUpdate(value -> value == 0 ? 0 : value - 1); + if (oldNewUpdateCallsReceived == 0) { + break; + } + numberOfUpdateCallsAtStartOfLastIteration = oldNewUpdateCallsReceived - 1; + } else { + /* + If we made no changes in the last iteration, + we can quit if no update calls were received in the meantime. + In that case, we can reset newUpdateCallsReceived as we have finished all necessary updates. + */ + final int finalNumberOfUpdateCallsAtStartOfLastIteration = numberOfUpdateCallsAtStartOfLastIteration; + int oldNewUpdateCallsReceived = newUpdateCallsReceived.getAndUpdate(value -> value == finalNumberOfUpdateCallsAtStartOfLastIteration ? 0 : value - 1); + if (oldNewUpdateCallsReceived == numberOfUpdateCallsAtStartOfLastIteration) { + break; + } + numberOfUpdateCallsAtStartOfLastIteration = oldNewUpdateCallsReceived - 1; + } + } + + // Reset madeChangesInLastIteration + madeChangesInLastIteration = false; + + // Get the threads + @Nullable BaseThread @NotNull [] threads = BaseThreadPool.getBaseThreads(); + + /* + Compute for each tier, for how many threads + the highest tier of any task on their stack is that tier. + Additionally, compute the threads for each tier that are waiting for some YieldingLock + (threads with no tasks on their stack cannot be waiting for a YieldingLock). + Additionally, compute the number of threads that are active (i.e. not waiting) + but that are not executing a task (i.e. do not have any tasks on their stack). + */ + Arrays.fill(numberOfThreadsActiveForTier, 0); + for (int tierI = 0; tierI < BaseTaskQueueTier.length; tierI++) { + threadsWaitingForLockForTier[tierI].clear(); + } + int activeAssistThreadsWithoutTask = 0; + for (BaseThread thread : threads) { + if (thread != null) { + BaseTaskQueueTier tier = thread.highestTierOfTaskOnStack; + if (tier == null) { + // The thread is doing nothing + if (thread.baseThreadIndex > 0 && !thread.isWaitingAndNeedsSignal()) { + // If it is doing nothing but not waiting, it is available to do anything + activeAssistThreadsWithoutTask++; + } + } else { + numberOfThreadsActiveForTier[tier.ordinal]++; + if (thread.lockWaitingFor != null) { + threadsWaitingForLockForTier[tier.ordinal].add(thread); + } + } + } + } + + /* + Compute the exclusive cumulative value of numberOfThreadsActiveForTier from above, + as being for how many threads the highest tier of any task on their stack + is a strictly lower priority tier. + */ + System.arraycopy(numberOfThreadsActiveForTier, 1, numberOfThreadsActiveForLowerThanTier, 0, BaseTaskQueueTier.length - 1); + for (int tierI = BaseTaskQueueTier.length - 2; tierI >= 0; tierI--) { + numberOfThreadsActiveForLowerThanTier[tierI] += numberOfThreadsActiveForLowerThanTier[tierI + 1]; + } + + /* + Compute the exclusive cumulative value of numberOfThreadsActiveForTier from below, + as being for how many threads the highest tier of any task on their stack + is a strictly higher priority tier. + */ + System.arraycopy(numberOfThreadsActiveForTier, 0, numberOfThreadsActiveForHigherThanTier, 1, BaseTaskQueueTier.length - 1); + for (int tierI = 2; tierI < BaseTaskQueueTier.length; tierI++) { + numberOfThreadsActiveForHigherThanTier[tierI] += numberOfThreadsActiveForHigherThanTier[tierI - 1]; + } + + /* + For each tier, compute the number of threads that should be active if there were tasks. + This can then later be compared to the actual number of active threads for that tier. + */ + for (int tierI = 0; tierI < BaseTaskQueueTier.length; tierI++) { + numberOfThreadsIntendedToBeActiveForTier[tierI] = BaseThreadPool.targetParallelism - (tierI == 0 ? 0 : activeAssistThreadsWithoutTask) - numberOfThreadsActiveForHigherThanTier[tierI] - Math.min(numberOfThreadsActiveForLowerThanTier[tierI], BaseThreadPool.maxUndisturbedLowerTierThreadCount); + } + // There can only be one active server thread + numberOfThreadsIntendedToBeActiveForTier[0] = Math.min(numberOfThreadsIntendedToBeActiveForTier[0], 1); + + { + final int finalActiveAssistThreadsWithoutTask = activeAssistThreadsWithoutTask; + MinecraftServer.THREAD_DEBUG_LOGGER.ifPresent(it -> it.info("Target parallelism = " + BaseThreadPool.targetParallelism + ", active threads without task = " + finalActiveAssistThreadsWithoutTask + ", active threads for tiers = " + Arrays.toString(numberOfThreadsActiveForTier) + ", number of threads intended to be active for tiers = " + Arrays.toString(numberOfThreadsIntendedToBeActiveForTier))); + } + + /* + * Determine the highest tier for which the number of threads that are active exceeds + * the number of threads that should be active if there were tasks. + * If none, set tierInExcessOrdinal to BaseTaskQueueTier#length. + */ + for (int tierI = 0;; tierI++) { + if (tierI == BaseTaskQueueTier.length || numberOfThreadsActiveForTier[tierI] > numberOfThreadsIntendedToBeActiveForTier[tierI]) { + tierInExcessOrdinal = tierI; + break; + } + } + + /* + Try to activate a thread, for higher to lower priority tier tasks, in order: + if a thread is activated, we continue with another update iteration, so that we make a + good-as-possible attempt to activate threads for higher priority tier tasks first. + */ + for (int tierI = 0; tierI < BaseTaskQueueTier.length; tierI++) { + // Only if we need to activate threads + if (numberOfThreadsActiveForTier[tierI] < numberOfThreadsIntendedToBeActiveForTier[tierI]) { + /* + Only if there may be tasks at all (which, if true, will be the reason provided when signalling a + thread), or if there is a thread already at this exact tier that is waiting for a YieldingLock. + */ + boolean thereAreTasks = false; + for (int spanI = 0; spanI < TaskSpan.length; spanI++) { + if (thereMayBeTasks[tierI][spanI].get() > 0) { + thereAreTasks = true; + break; + } + } + if (thereAreTasks || !threadsWaitingForLockForTier[tierI].isEmpty()) { + + /* + * We attempt to wake up a thread that is sleeping, + * or add a new thread to start running. + * Of course, we can only choose a thread that could poll a task. + * We only choose a thread that can accept yielding tasks, even if + * the added task is yield-free, so that we have a lower chance of + * the chosen thread getting stuck again quickly. + * Out of the possible threads, we attempt to choose one that is waiting for a YieldingLock + * that is available, so that we have a thread owning this lock as quickly as possible again, + * making the next time it is released again sooner as well. + * Out of the possible threads that are not waiting for a lock, + * we attempt to choose one with non-zero yield depth over any with zero yield depth, + * since we must later wake up this thread anyway. Then, we attempt to choose one with the + * lowest possible yield depth, so that it can still keep yielding as much as possible. + */ + /* + Special case: only the server thread can start SERVER tasks, + and we never activate it for other tiers, because it could only start tiny tasks. + */ + int tryThreadsStart, tryThreadsEnd; + if (tierI == 0) { + tryThreadsStart = 0; + tryThreadsEnd = 1; + } else { + tryThreadsStart = 1; + tryThreadsEnd = threads.length; + } + while (true) { + // Find the best thread to activate + int threadIToUpdate = -1; + boolean threadIToUpdateIsWaitingForAvailableYieldingLock = false; + int threadIToUpdateYieldDepth = 0; + int threadIToUpdateTierOrdinalOrLength = 0; + for (int threadI = tryThreadsStart; threadI < tryThreadsEnd; threadI++) { + BaseThread thread = threads[threadI]; + if (thread != null) { + if (thread.isWaitingAndNeedsSignal() && thread.canStartYieldingTasks) { + /* + Tasks of a certain tier may yield to tasks of the same or a higher + tier, and they may also yield to tiny tasks of a lower tier. + We do not want to wake up a thread just for tiny tasks + unless it has zero yield depth, + so we only activate threads that have either no tasks on their stack, + or only tasks of the same or a lower tier, where a lower priority tier + is preferred (but not as important as the yield depth). + Of course, this only takes into account tasks, and we may also + activate threads due to them waiting on an available YieldingLock. + */ + var highestTierOfTaskOnStack = thread.highestTierOfTaskOnStack; + var highestTierOfTaskOnStackOrdinalOrLength = highestTierOfTaskOnStack == null ? BaseTaskQueueTier.length : highestTierOfTaskOnStack.ordinal; + @Nullable YieldingLock lockWaitingFor = thread.lockWaitingFor; + boolean isThreadWaitingForAvailableYieldingLock = lockWaitingFor != null && !lockWaitingFor.isLocked(); + if (isThreadWaitingForAvailableYieldingLock || highestTierOfTaskOnStack == null || highestTierOfTaskOnStack.ordinal >= tierI) { + boolean isBestChoice = false; + int yieldDepth = thread.yieldDepth; + if (threadIToUpdate == -1) { + isBestChoice = true; + } else if (isThreadWaitingForAvailableYieldingLock != threadIToUpdateIsWaitingForAvailableYieldingLock) { + isBestChoice = isThreadWaitingForAvailableYieldingLock; + } else if (threadIToUpdateYieldDepth == 0 && yieldDepth != 0) { + isBestChoice = true; + } else if (yieldDepth != 0) { + if (yieldDepth < threadIToUpdateYieldDepth) { + isBestChoice = true; + } else if (highestTierOfTaskOnStackOrdinalOrLength > threadIToUpdateTierOrdinalOrLength) { + isBestChoice = true; + } + } + if (isBestChoice) { + threadIToUpdate = threadI; + threadIToUpdateIsWaitingForAvailableYieldingLock = isThreadWaitingForAvailableYieldingLock; + threadIToUpdateYieldDepth = yieldDepth; + threadIToUpdateTierOrdinalOrLength = highestTierOfTaskOnStackOrdinalOrLength; + } + } + } + } + } + if (threadIToUpdate == -1) { + // No valid thread was found + break; + } + // Check if the thread still seems valid and attempt to activate it + BaseThread thread = threads[threadIToUpdate]; + if (thread.isWaitingAndNeedsSignal() && thread.canStartYieldingTasks) { + // Wake up the thread + if (thread.signal(thereAreTasks ? SignalReason.TASK : SignalReason.YIELDING_LOCK)) { + // Do another update + madeChangesInLastIteration = true; + continue updateWhileNecessary; + } + } + /* + The thread was not valid to activate anymore, or not activated, + so we attempt to find a valid thread again. + */ + } + + // Because no thread was activated, we add one (only if we were looking for an AssistThread) + if (tierI != 0) { + BaseThreadPool.addAssistThread(); + // Do another update + madeChangesInLastIteration = true; + continue updateWhileNecessary; + } + + } + } + } + + } + } + +} diff --git a/src/main/java/org/galemc/gale/executor/thread/pool/BaseThreadPool.java b/src/main/java/org/galemc/gale/executor/thread/pool/BaseThreadPool.java new file mode 100644 index 0000000000000000000000000000000000000000..36c9faaf2d431dbc5a173a3821752725b497dd6c --- /dev/null +++ b/src/main/java/org/galemc/gale/executor/thread/pool/BaseThreadPool.java @@ -0,0 +1,219 @@ +// Gale - base thread pool + +package org.galemc.gale.executor.thread.pool; + +import net.minecraft.server.MinecraftServer; +import org.galemc.gale.executor.queue.BaseTaskQueueTier; +import org.galemc.gale.executor.thread.AssistThread; +import org.galemc.gale.executor.thread.BaseThread; +import org.galemc.gale.executor.thread.ServerThread; +import org.galemc.gale.util.CPUCoresEstimation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; + +/** + * A pool of threads that can perform tasks to assist the current {@link ServerThread}. These tasks can be of + * different {@linkplain BaseTaskQueueTier tiers}. + *
    + * This pool intends to keep {@link #targetParallelism} threads active at any time, + * which includes a potentially active {@link ServerThread}. + *
    + * As such, this pool is closely intertwined with the {@link ServerThread}. This pool can not control the + * {@link ServerThread} in any way, but it is responsible for signalling the {@link ServerThread} when tasks become + * available in a {@link BaseTaskQueueTier#SERVER} task queue, and for listening for when the {@link ServerThread} + * becomes (in)active in order to update the number of active {@link AssistThread}s accordingly. + *

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