mirror of
https://github.com/Dreeam-qwq/Gale.git
synced 2025-12-22 08:19:31 +00:00
6296 lines
292 KiB
Diff
6296 lines
292 KiB
Diff
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
From: Martijn Muijsers <martijnmuijsers@live.nl>
|
|
Date: Sun, 29 Jan 2023 23:41:12 +0100
|
|
Subject: [PATCH] Base thread pool
|
|
|
|
License: AGPL-3.0 (https://www.gnu.org/licenses/agpl-3.0.html)
|
|
Gale - https://galemc.org
|
|
|
|
diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java
|
|
index 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/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<G, W> {
|
|
YamlConfiguration global = YamlConfiguration.loadConfiguration(this.globalFolder.resolve(this.globalConfigFileName).toFile());
|
|
ConfigurationSection worlds = global.createSection(legacyWorldsSectionKey);
|
|
worlds.set(legacyWorldDefaultsSectionKey, YamlConfiguration.loadConfiguration(this.globalFolder.resolve(this.defaultWorldConfigFileName).toFile()));
|
|
- for (ServerLevel level : server.getAllLevels()) {
|
|
+ for (ServerLevel level : server.getAllLevelsArray()) { // Gale - base thread 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<GlobalConfiguration, Wor
|
|
try {
|
|
this.initializeGlobalConfiguration(reloader(this.globalConfigClass, GlobalConfiguration.get()));
|
|
this.initializeWorldDefaultsConfiguration();
|
|
- for (ServerLevel level : server.getAllLevels()) {
|
|
+ for (ServerLevel level : server.getAllLevelsArray()) { // Gale - base thread pool - optimize server levels
|
|
this.createWorldConfig(createWorldContextMap(level), reloader(this.worldConfigClass, level.paperConfig()));
|
|
}
|
|
} catch (Exception ex) {
|
|
diff --git a/src/main/java/io/papermc/paper/util/MCUtil.java b/src/main/java/io/papermc/paper/util/MCUtil.java
|
|
index 3fad7e58a1461d897526d63efd27075f044f2962..4b8da38db72d7ebc2d498ec3d711d3b30911096c 100644
|
|
--- a/src/main/java/io/papermc/paper/util/MCUtil.java
|
|
+++ b/src/main/java/io/papermc/paper/util/MCUtil.java
|
|
@@ -35,6 +35,7 @@ import org.bukkit.block.BlockFace;
|
|
import org.bukkit.craftbukkit.CraftWorld;
|
|
import org.bukkit.craftbukkit.util.Waitable;
|
|
import org.spigotmc.AsyncCatcher;
|
|
+import org.galemc.gale.executor.queue.BaseTaskQueues;
|
|
|
|
import javax.annotation.Nonnull;
|
|
import javax.annotation.Nullable;
|
|
@@ -47,6 +48,7 @@ import java.util.concurrent.CompletableFuture;
|
|
import java.util.concurrent.ExecutionException;
|
|
import java.util.concurrent.LinkedBlockingQueue;
|
|
import java.util.concurrent.ThreadPoolExecutor;
|
|
+import java.util.concurrent.Executor;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.concurrent.TimeoutException;
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
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/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 <T extends PacketListener> void ensureRunningOnSameThread(Packet<T> packet, T listener, ServerLevel world) throws RunningOnDifferentThreadException {
|
|
- PacketUtils.ensureRunningOnSameThread(packet, listener, (BlockableEventLoop) world.getServer());
|
|
+ PacketUtils.ensureRunningOnSameThread(packet, listener, world.getServer()); // Gale - base thread pool
|
|
}
|
|
|
|
- public static <T extends PacketListener> void ensureRunningOnSameThread(Packet<T> packet, T listener, BlockableEventLoop<?> engine) throws RunningOnDifferentThreadException {
|
|
+ public static <T extends PacketListener> void ensureRunningOnSameThread(Packet<T> packet, T listener, AbstractBlockableEventLoop engine) throws RunningOnDifferentThreadException { // Gale - base thread 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..ac12cabaf15bc3520ff74d09faa48a135c63f23c 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<TickTask> 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<Logger> 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<TickTa
|
|
private int port;
|
|
private final LayeredRegistryAccess<RegistryLayer> registries;
|
|
private Map<ResourceKey<Level>, 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<TickTa
|
|
private long lastOverloadWarning;
|
|
protected final Services services;
|
|
private long lastServerStatus;
|
|
- public final Thread serverThread;
|
|
- private long nextTickTime;
|
|
- private long delayedTasksMaxNextTickTime;
|
|
- private boolean mayHaveDelayedTasks;
|
|
+ public static OriginalServerThread serverThread; // Gale - base thread pool - rename, instance -> 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<TickTa
|
|
public java.util.Queue<Runnable> processQueue = new java.util.concurrent.ConcurrentLinkedQueue<Runnable>();
|
|
public int autosavePeriod;
|
|
public Commands vanillaCommandDispatcher;
|
|
- public boolean forceTicks; // Paper
|
|
+ public volatile boolean forceTicks; // Paper // Gale - base thread pool - make fields volatile
|
|
// CraftBukkit end
|
|
// Spigot start
|
|
public static final int TPS = 20;
|
|
@@ -303,9 +409,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
public volatile boolean abnormalExit = false; // Paper
|
|
public boolean isIteratingOverLevels = false; // Paper
|
|
|
|
- public static <S extends MinecraftServer> S spin(Function<Thread, S> serverFactory) {
|
|
+ public static <S extends MinecraftServer> S spin(Function<OriginalServerThread, S> serverFactory) { // Gale - base thread pool
|
|
AtomicReference<S> atomicreference = new AtomicReference();
|
|
- Thread thread = new io.papermc.paper.util.TickThread(() -> { // Paper - rewrite chunk system
|
|
+ OriginalServerThread thread = new OriginalServerThread(() -> { // Paper - rewrite chunk system // Gale - base thread pool
|
|
((MinecraftServer) atomicreference.get()).runServer();
|
|
}, "Server thread");
|
|
|
|
@@ -324,16 +430,19 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
return s0;
|
|
}
|
|
|
|
- public MinecraftServer(OptionSet options, WorldLoader.DataLoadContext worldLoader, Thread thread, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PackRepository resourcepackrepository, WorldStem worldstem, Proxy proxy, DataFixer datafixer, Services services, ChunkProgressListenerFactory worldloadlistenerfactory) {
|
|
- super("Server");
|
|
+ // Gale start - base thread pool
|
|
+ public MinecraftServer(OptionSet options, WorldLoader.DataLoadContext worldLoader, OriginalServerThread thread, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PackRepository resourcepackrepository, WorldStem worldstem, Proxy proxy, DataFixer datafixer, Services services, ChunkProgressListenerFactory worldloadlistenerfactory) {
|
|
+ super();
|
|
+ // Gale end - base thread pool
|
|
SERVER = this; // Paper - better singleton
|
|
+ isConstructed = true; // Gale - base thread pool
|
|
this.status = new ServerStatus();
|
|
this.random = RandomSource.create();
|
|
this.port = -1;
|
|
this.levels = Maps.newLinkedHashMap();
|
|
this.running = true;
|
|
this.tickTimes = new long[100];
|
|
- this.nextTickTime = Util.getMillis();
|
|
+ this.setNextTickTime(Util.getMillis()); // Gale - base thread pool
|
|
this.scoreboard = new ServerScoreboard(this);
|
|
this.customBossEvents = new CustomBossEvents();
|
|
this.frameTimer = new FrameTimer();
|
|
@@ -359,7 +468,10 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
HolderGetter<Block> holdergetter = this.registries.compositeAccess().registryOrThrow(Registries.BLOCK).asLookup().filterFeatures(this.worldData.enabledFeatures());
|
|
|
|
this.structureTemplateManager = new StructureTemplateManager(worldstem.resourceManager(), convertable_conversionsession, datafixer, holdergetter);
|
|
- this.serverThread = thread;
|
|
+ // Gale start - base thread 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<TickTa
|
|
}
|
|
}
|
|
this.forceDifficulty();
|
|
- for (ServerLevel worldserver : this.getAllLevels()) {
|
|
+ for (ServerLevel worldserver : this.getAllLevelsArray()) { // Gale - base thread pool - optimize server levels
|
|
this.prepareLevels(worldserver.getChunkSource().chunkMap.progressListener, worldserver);
|
|
//worldserver.entityManager.tick(); // SPIGOT-6526: Load pending entities so they are available to the API // Paper - rewrite chunk system, not required to "tick" anything
|
|
this.server.getPluginManager().callEvent(new org.bukkit.event.world.WorldLoadEvent(worldserver.getWorld()));
|
|
@@ -758,7 +870,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
//ChunkProviderServer chunkproviderserver = worldserver.getChunkProvider(); // Paper - move up
|
|
|
|
chunkproviderserver.getLightEngine().setTaskPerBatch(500);
|
|
- this.nextTickTime = Util.getMillis();
|
|
+ this.setNextTickTime(Util.getMillis()); // Gale - base thread pool
|
|
// Paper start - configurable spawn reason
|
|
int radiusBlocks = worldserver.paperConfig().spawn.keepSpawnLoadedRange * 16;
|
|
int radiusChunks = radiusBlocks / 16 + ((radiusBlocks & 15) != 0 ? 1 : 0);
|
|
@@ -802,6 +914,11 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
worldserver.setSpawnSettings(worldserver.serverLevelData.getDifficulty() != Difficulty.PEACEFUL && ((DedicatedServer) this).settings.getProperties().spawnMonsters, this.isSpawningAnimals()); // Paper - per level difficulty (from setDifficulty(ServerLevel, Difficulty, boolean))
|
|
|
|
this.forceTicks = false;
|
|
+ // Gale start - base thread pool
|
|
+ if (isWaitingUntilNextTick) {
|
|
+ signalServerThreadIfCurrentManagedBlockStopConditionBecameTrue();
|
|
+ }
|
|
+ // Gale end - base thread pool
|
|
// CraftBukkit end
|
|
}
|
|
|
|
@@ -828,8 +945,12 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
// Paper end - rewrite chunk system - add close param
|
|
boolean flag3 = false;
|
|
|
|
- for (Iterator iterator = this.getAllLevels().iterator(); iterator.hasNext(); flag3 = true) {
|
|
- ServerLevel worldserver = (ServerLevel) iterator.next();
|
|
+ // Gale start - base thread pool - optimize server levels
|
|
+ ServerLevel[] worldservers = this.getAllLevelsArray();
|
|
+ for (int worldserverI = 0; worldserverI < worldservers.length; flag3 = true) {
|
|
+ ServerLevel worldserver = worldservers[worldserverI];
|
|
+ worldserverI++;
|
|
+ // Gale end - base thread pool - optimize server levels
|
|
|
|
if (!suppressLogs) {
|
|
MinecraftServer.LOGGER.info("Saving chunks for level '{}'/{}", worldserver, worldserver.dimension().location());
|
|
@@ -853,14 +974,6 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
*/
|
|
// CraftBukkit end
|
|
if (flush) {
|
|
- Iterator iterator1 = this.getAllLevels().iterator();
|
|
-
|
|
- while (iterator1.hasNext()) {
|
|
- ServerLevel worldserver2 = (ServerLevel) iterator1.next();
|
|
-
|
|
- //MinecraftServer.LOGGER.info("ThreadedAnvilChunkStorage ({}): All chunks are saved", worldserver2.getChunkSource().chunkMap.getStorageName()); // Paper - move up
|
|
- }
|
|
-
|
|
MinecraftServer.LOGGER.info("ThreadedAnvilChunkStorage: All dimensions are saved");
|
|
}
|
|
|
|
@@ -887,7 +1000,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
}
|
|
|
|
// CraftBukkit start
|
|
- private boolean hasStopped = false;
|
|
+ public boolean hasStopped = false; // Gale - base thread pool - private -> 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<TickTa
|
|
*/
|
|
MinecraftServer.LOGGER.info("Stopping main thread (Ignore any thread death message you see! - DO NOT REPORT THREAD DEATH TO PAPER - If you think this is a Gale bug, please report it at https://github.com/GaleMC/Gale/issues )");
|
|
// Gale end - branding changes
|
|
- while (this.getRunningThread().isAlive()) {
|
|
- this.getRunningThread().stop();
|
|
+ // Gale start - base thread pool
|
|
+ while (serverThread.isAlive()) {
|
|
+ serverThread.stop();
|
|
+ // Gale end - base thread pool
|
|
try {
|
|
Thread.sleep(1);
|
|
} catch (InterruptedException e) {}
|
|
@@ -948,12 +1063,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
}
|
|
|
|
MinecraftServer.LOGGER.info("Saving worlds");
|
|
- Iterator iterator = this.getAllLevels().iterator();
|
|
-
|
|
- ServerLevel worldserver;
|
|
-
|
|
- while (iterator.hasNext()) {
|
|
- worldserver = (ServerLevel) iterator.next();
|
|
+ for (ServerLevel worldserver : this.getAllLevelsArray()) { // Gale - base thread pool - optimize server levels
|
|
if (worldserver != null) {
|
|
worldserver.noSave = false;
|
|
}
|
|
@@ -1017,7 +1127,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
this.running = false;
|
|
if (waitForShutdown) {
|
|
try {
|
|
- this.serverThread.join();
|
|
+ serverThread.join(); // Gale - base thread pool
|
|
} catch (InterruptedException interruptedexception) {
|
|
MinecraftServer.LOGGER.error("Error while shutting down", interruptedexception);
|
|
}
|
|
@@ -1091,6 +1201,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
public static long lastTickOversleepTime;
|
|
// Gale end - YAPFA - last tick time
|
|
|
|
+ @OriginalServerThreadOnly // Gale - base thread pool
|
|
protected void runServer() {
|
|
try {
|
|
long serverStartTime = Util.getNanos(); // Paper
|
|
@@ -1098,7 +1209,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
throw new IllegalStateException("Failed to initialize server");
|
|
}
|
|
|
|
- this.nextTickTime = Util.getMillis();
|
|
+ this.setNextTickTime(Util.getMillis()); // Gale - base thread pool
|
|
this.status.setDescription(Component.literal(this.motd));
|
|
this.status.setVersion(new ServerStatus.Version(SharedConstants.getCurrentVersion().getName(), SharedConstants.getCurrentVersion().getProtocolVersion()));
|
|
this.status.setEnforcesSecureChat(this.enforceSecureProfile());
|
|
@@ -1135,7 +1246,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
|
|
if (this.server.getWarnOnOverload()) // CraftBukkit
|
|
MinecraftServer.LOGGER.warn("Can't keep up! Is the server overloaded? Running {}ms or {} ticks behind", i, j);
|
|
- this.nextTickTime += j * 50L;
|
|
+ this.setNextTickTime(this.nextTickTime + j * 50L); // Gale - base thread pool
|
|
this.lastOverloadWarning = this.nextTickTime;
|
|
}
|
|
|
|
@@ -1159,12 +1270,11 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
|
|
//MinecraftServer.currentTick = (int) (System.currentTimeMillis() / 50); // CraftBukkit // Paper - don't overwrite current tick time
|
|
lastTick = curTime;
|
|
- this.nextTickTime += 50L;
|
|
+ this.setNextTickTime(this.nextTickTime + 50L); // Gale - base thread pool
|
|
long tickProperStart = System.nanoTime(); // Gale - YAPFA - last tick time
|
|
this.tickServer(this::haveTime);
|
|
lastTickProperTime = (System.nanoTime() - tickProperStart) / 1000000L; // Gale - YAPFA - last tick time
|
|
- this.mayHaveDelayedTasks = true;
|
|
- this.delayedTasksMaxNextTickTime = Math.max(Util.getMillis() + 50L, this.nextTickTime);
|
|
+ this.setDelayedTasksMaxNextTickTime(Math.max(Util.getMillis() + 50L, this.nextTickTime)); // Gale - base thread pool
|
|
this.waitUntilNextTick();
|
|
this.isReady = true;
|
|
JvmProfiler.INSTANCE.onServerTick(this.averageTickTime);
|
|
@@ -1245,7 +1355,46 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
return crashreport;
|
|
}
|
|
|
|
- private boolean haveTime() {
|
|
+ // Gale start - base thread pool
|
|
+
|
|
+ /**
|
|
+ * The return value of this method is slightly heuristic in when it is computed: it may be invalidated by
|
|
+ * other threads during its execution or immediately upon returning.
|
|
+ *
|
|
+ * @return Whether there are potentially main-thread-only tasks scheduled in some queue.
|
|
+ */
|
|
+ private static boolean mayHaveDelayedTasks() {
|
|
+ // First check the flag to skip this check
|
|
+ if (nextTimeAssumeWeMayHaveDelayedTasks) {
|
|
+ nextTimeAssumeWeMayHaveDelayedTasks = false;
|
|
+ mayHaveDelayedTasksIsCurrentlyComputed = false;
|
|
+ return true;
|
|
+ }
|
|
+ // If we still have a valid computation result, use it
|
|
+ if (mayHaveDelayedTasksIsCurrentlyComputed) {
|
|
+ return lastComputedMayHaveDelayedTasks;
|
|
+ }
|
|
+ // Compute the result and save it
|
|
+ lastComputedMayHaveDelayedTasks = false;
|
|
+ if (ScheduledServerThreadTaskQueues.hasTasks(true)) {
|
|
+ lastComputedMayHaveDelayedTasks = true;
|
|
+ } else {
|
|
+ for (ServerLevel level : SERVER.getAllLevelsArray()) {
|
|
+ if (level.chunkSource.mainThreadProcessor.hasPendingTasks() || level.chunkTaskScheduler.mainThreadExecutor.hasScheduledUncompletedTasksVolatile()) {
|
|
+ lastComputedMayHaveDelayedTasks = true;
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ mayHaveDelayedTasksIsCurrentlyComputed = true;
|
|
+ if (!lastComputedMayHaveDelayedTasks) {
|
|
+ serverThread.signal(null);
|
|
+ }
|
|
+ return lastComputedMayHaveDelayedTasks;
|
|
+ }
|
|
+ // Gale end - base thread pool
|
|
+
|
|
+ public boolean haveTime() { // Gale - base thread pool - private -> public
|
|
// Paper start
|
|
if (this.forceTicks) {
|
|
return true;
|
|
@@ -1253,13 +1402,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
// Paper end
|
|
// CraftBukkit start
|
|
if (isOversleep) return canOversleep();// Paper - because of our changes, this logic is broken
|
|
- return this.forceTicks || this.runningTask() || Util.getMillis() < (this.mayHaveDelayedTasks ? this.delayedTasksMaxNextTickTime : this.nextTickTime);
|
|
+ return this.forceTicks || this.runningTask() || Util.getMillis() < (mayHaveDelayedTasks() ? this.delayedTasksMaxNextTickTime : this.nextTickTime); // Gale - base thread pool
|
|
}
|
|
|
|
// Paper start
|
|
- boolean isOversleep = false;
|
|
+ public volatile boolean isOversleep = false; // Gale - base thread pool - make fields volatile, package -> 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 +1417,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
// Paper end
|
|
|
|
private void executeModerately() {
|
|
- this.runAllTasks();
|
|
+ this.runAllMainThreadTasksForAllTicks(); // Gale - base thread pool
|
|
java.util.concurrent.locks.LockSupport.parkNanos("executing tasks", 1000L);
|
|
}
|
|
// CraftBukkit end
|
|
@@ -1276,61 +1425,20 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
protected void waitUntilNextTick() {
|
|
//this.executeAll(); // Paper - move this into the tick method for timings
|
|
long tickOversleepStart = System.nanoTime(); // Gale - YAPFA - last tick time
|
|
+ // Gale start - base thread pool
|
|
+ isWaitingUntilNextTick = true;
|
|
+ isInSpareTime = true;
|
|
+ // Gale end - base thread pool
|
|
this.managedBlock(() -> {
|
|
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<File> optional = Optional.of(this.getFile("server-icon.png")).filter(File::isFile);
|
|
|
|
@@ -1378,14 +1486,19 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
|
|
// Paper start - move oversleep into full server tick
|
|
isOversleep = true;MinecraftTimings.serverOversleep.startTiming();
|
|
+ isInSpareTime = true; // Gale - base thread pool
|
|
this.managedBlock(() -> {
|
|
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 +1533,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
if (playerSaveInterval > 0) {
|
|
this.playerList.saveAll(playerSaveInterval);
|
|
}
|
|
- for (ServerLevel level : this.getAllLevels()) {
|
|
+ for (ServerLevel level : this.getAllLevelsArray()) { // Gale - base thread pool - optimize server levels
|
|
if (level.paperConfig().chunks.autoSaveInterval.value() > 0) {
|
|
level.saveIncrementally(fullSave);
|
|
}
|
|
@@ -1432,7 +1545,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
io.papermc.paper.util.CachedLists.reset(); // Paper
|
|
// Paper start - move executeAll() into full server tick timing
|
|
try (co.aikar.timings.Timing ignored = MinecraftTimings.processTasksTimer.startTiming()) {
|
|
- this.runAllTasks();
|
|
+ this.runAllTasksWithinTimeOrForCurrentTick();
|
|
}
|
|
// Paper end
|
|
// Paper start
|
|
@@ -1476,7 +1589,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
MinecraftTimings.timeUpdateTimer.startTiming(); // Spigot // Paper
|
|
// Send time updates to everyone, it will get the right time from the world the player is in.
|
|
// Paper start - optimize time updates
|
|
- for (final ServerLevel world : this.getAllLevels()) {
|
|
+ for (final ServerLevel world : this.getAllLevelsArray()) { // Gale - base thread pool - optimize server levels
|
|
final boolean doDaylight = world.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT);
|
|
final long dayTime = world.getDayTime();
|
|
long worldTime = world.getGameTime();
|
|
@@ -1496,9 +1609,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
MinecraftTimings.timeUpdateTimer.stopTiming(); // Spigot // Paper
|
|
|
|
this.isIteratingOverLevels = true; // Paper
|
|
- Iterator iterator = this.getAllLevels().iterator(); // Paper - move down
|
|
- while (iterator.hasNext()) {
|
|
- ServerLevel worldserver = (ServerLevel) iterator.next();
|
|
+ for (ServerLevel worldserver : this.getAllLevelsArray()) { // Paper - move down // Gale - base thread pool - optimize server levels
|
|
worldserver.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper
|
|
worldserver.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper
|
|
net.minecraft.world.level.block.entity.HopperBlockEntity.skipHopperEvents = worldserver.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper
|
|
@@ -1569,7 +1680,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
}
|
|
|
|
public boolean isShutdown() {
|
|
- return !this.serverThread.isAlive();
|
|
+ return !serverThread.isAlive(); // Gale - base thread pool
|
|
}
|
|
|
|
public File getFile(String path) {
|
|
@@ -1577,7 +1688,12 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
}
|
|
|
|
public final ServerLevel overworld() {
|
|
- return (ServerLevel) this.levels.get(Level.OVERWORLD);
|
|
+ // Gale start - base thread pool - optimize server levels
|
|
+ if (this.overworld == null) {
|
|
+ this.overworld = (ServerLevel) this.levels.get(Level.OVERWORLD);
|
|
+ }
|
|
+ return this.overworld;
|
|
+ // Gale end - base thread pool - optimize server levels
|
|
}
|
|
|
|
@Nullable
|
|
@@ -1591,6 +1707,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
Map<ResourceKey<Level>, ServerLevel> newLevels = Maps.newLinkedHashMap(oldLevels);
|
|
newLevels.put(level.dimension(), level);
|
|
this.levels = Collections.unmodifiableMap(newLevels);
|
|
+ // Gale start - base thread 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 +1721,14 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
Map<ResourceKey<Level>, ServerLevel> newLevels = Maps.newLinkedHashMap(oldLevels);
|
|
newLevels.remove(level.dimension());
|
|
this.levels = Collections.unmodifiableMap(newLevels);
|
|
+ // Gale start - base thread 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 +1736,14 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
return this.levels.keySet();
|
|
}
|
|
|
|
+ // Gale start - base thread pool - optimize server levels
|
|
+ public ServerLevel[] getAllLevelsArray() {
|
|
+ return this.levelArray;
|
|
+ }
|
|
+ // Gale end - base thread pool - optimize server levels
|
|
+
|
|
public Iterable<ServerLevel> getAllLevels() {
|
|
- return this.levels.values();
|
|
+ return this.levels == null ? Collections.emptyList() : this.levels.values(); // Gale - base thread pool
|
|
}
|
|
|
|
public String getServerVersion() {
|
|
@@ -1726,10 +1863,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
}
|
|
|
|
private void updateMobSpawningFlags() {
|
|
- Iterator iterator = this.getAllLevels().iterator();
|
|
-
|
|
- while (iterator.hasNext()) {
|
|
- ServerLevel worldserver = (ServerLevel) iterator.next();
|
|
+ for (ServerLevel worldserver : this.getAllLevelsArray()) { // Gale - base thread pool - optimize server levels
|
|
|
|
worldserver.setSpawnSettings(worldserver.serverLevelData.getDifficulty() != Difficulty.PEACEFUL && ((DedicatedServer) this).settings.getProperties().spawnMonsters, this.isSpawningAnimals()); // Paper - per level difficulty (from setDifficulty(ServerLevel, Difficulty, boolean))
|
|
}
|
|
@@ -1928,25 +2062,6 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
return 29999984;
|
|
}
|
|
|
|
- @Override
|
|
- public boolean scheduleExecutables() {
|
|
- return super.scheduleExecutables() && !this.isStopped();
|
|
- }
|
|
-
|
|
- @Override
|
|
- public void executeIfPossible(Runnable runnable) {
|
|
- if (this.isStopped()) {
|
|
- throw new RejectedExecutionException("Server already shutting down");
|
|
- } else {
|
|
- super.executeIfPossible(runnable);
|
|
- }
|
|
- }
|
|
-
|
|
- @Override
|
|
- public Thread getRunningThread() {
|
|
- return this.serverThread;
|
|
- }
|
|
-
|
|
public int getCompressionThreshold() {
|
|
return 256;
|
|
}
|
|
@@ -2013,7 +2128,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
net.minecraft.world.item.alchemy.PotionBrewing.reload(); // Paper
|
|
new io.papermc.paper.event.server.ServerResourcesReloadedEvent(cause).callEvent(); // Paper
|
|
// Paper start
|
|
- if (Thread.currentThread() != this.serverThread) {
|
|
+ if (Thread.currentThread() != serverThread) { // Gale - base thread pool
|
|
return;
|
|
}
|
|
// this.getPlayerList().saveAll(); // Paper - we don't need to save everything, just advancements
|
|
@@ -2246,7 +2361,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
BufferedWriter bufferedwriter = Files.newBufferedWriter(path);
|
|
|
|
try {
|
|
- bufferedwriter.write(String.format(Locale.ROOT, "pending_tasks: %d\n", this.getPendingTasksCount()));
|
|
+ bufferedwriter.write(String.format(Locale.ROOT, "pending_tasks: %d\n", ScheduledServerThreadTaskQueues.getTaskCount())); // Gale - base thread pool
|
|
bufferedwriter.write(String.format(Locale.ROOT, "average_tick_time: %f\n", this.getAverageTickTime()));
|
|
bufferedwriter.write(String.format(Locale.ROOT, "tick_times: %s\n", Arrays.toString(this.tickTimes)));
|
|
bufferedwriter.write(String.format(Locale.ROOT, "queue: %s\n", Util.backgroundExecutor()));
|
|
@@ -2432,7 +2547,6 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
}
|
|
|
|
// CraftBukkit start
|
|
- @Override
|
|
public boolean isSameThread() {
|
|
return io.papermc.paper.util.TickThread.isTickThread(); // Paper - rewrite chunk system
|
|
}
|
|
@@ -2570,7 +2684,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
|
// give all worlds a fair chance at by targetting them all.
|
|
// if we execute too many tasks, that's fine - we have logic to correctly handle overuse of allocated time.
|
|
boolean executed = false;
|
|
- for (ServerLevel world : this.getAllLevels()) {
|
|
+ for (ServerLevel world : this.getAllLevelsArray()) { // Gale - base thread pool - optimize server levels
|
|
long currTime = System.nanoTime();
|
|
if (currTime - world.lastMidTickExecuteFailure <= TASK_EXECUTION_FAILURE_BACKOFF) {
|
|
continue;
|
|
diff --git a/src/main/java/net/minecraft/server/commands/TimeCommand.java b/src/main/java/net/minecraft/server/commands/TimeCommand.java
|
|
index f0a7a8df3caa2ea765bb0a87cfede71d0995d276..16f3475b059d2b6b85d2b342e84ab32de8e86ac0 100644
|
|
--- a/src/main/java/net/minecraft/server/commands/TimeCommand.java
|
|
+++ b/src/main/java/net/minecraft/server/commands/TimeCommand.java
|
|
@@ -51,10 +51,11 @@ public class TimeCommand {
|
|
}
|
|
|
|
public static int setTime(CommandSourceStack source, int time) {
|
|
- Iterator iterator = io.papermc.paper.configuration.GlobalConfiguration.get().commands.timeCommandAffectsAllWorlds ? source.getServer().getAllLevels().iterator() : com.google.common.collect.Iterators.singletonIterator(source.getLevel()); // CraftBukkit - SPIGOT-6496: Only set the time for the world the command originates in // Paper - add config option for spigot's change
|
|
+ // Gale start - base thread pool - optimize server levels
|
|
+ ServerLevel[] worldservers = io.papermc.paper.configuration.GlobalConfiguration.get().commands.timeCommandAffectsAllWorlds ? source.getServer().getAllLevelsArray() : new ServerLevel[]{source.getLevel()}; // CraftBukkit - SPIGOT-6496: Only set the time for the world the command originates in // Paper - add config option for spigot's change
|
|
|
|
- while (iterator.hasNext()) {
|
|
- ServerLevel worldserver = (ServerLevel) iterator.next();
|
|
+ for (ServerLevel worldserver : worldservers) {
|
|
+ // Gale end - base thread pool - optimize server levels
|
|
|
|
// CraftBukkit start
|
|
TimeSkipEvent event = new TimeSkipEvent(worldserver.getWorld(), TimeSkipEvent.SkipReason.COMMAND, time - worldserver.getDayTime());
|
|
@@ -70,10 +71,11 @@ public class TimeCommand {
|
|
}
|
|
|
|
public static int addTime(CommandSourceStack source, int time) {
|
|
- Iterator iterator = io.papermc.paper.configuration.GlobalConfiguration.get().commands.timeCommandAffectsAllWorlds ? source.getServer().getAllLevels().iterator() : com.google.common.collect.Iterators.singletonIterator(source.getLevel()); // CraftBukkit - SPIGOT-6496: Only set the time for the world the command originates in // Paper - add config option for spigot's change
|
|
+ // Gale start - base thread pool - optimize server levels
|
|
+ ServerLevel[] worldservers = io.papermc.paper.configuration.GlobalConfiguration.get().commands.timeCommandAffectsAllWorlds ? source.getServer().getAllLevelsArray() : new ServerLevel[]{source.getLevel()}; // CraftBukkit - SPIGOT-6496: Only set the time for the world the command originates in // Paper - add config option for spigot's change
|
|
|
|
- while (iterator.hasNext()) {
|
|
- ServerLevel worldserver = (ServerLevel) iterator.next();
|
|
+ for (ServerLevel worldserver : worldservers) {
|
|
+ // Gale end - base thread pool - optimize server levels
|
|
|
|
// CraftBukkit start
|
|
TimeSkipEvent event = new TimeSkipEvent(worldserver.getWorld(), TimeSkipEvent.SkipReason.COMMAND, time);
|
|
diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
|
|
index eed9f125df46b616b7234a2d669971bc51bc231b..3056d66779d236071d52d731a1c4e56aad0746c5 100644
|
|
--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
|
|
+++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
|
|
@@ -49,6 +49,7 @@ import net.minecraft.world.level.block.entity.SkullBlockEntity;
|
|
import net.minecraft.world.level.storage.LevelStorageSource;
|
|
import org.galemc.gale.command.GaleCommands;
|
|
import org.galemc.gale.configuration.GaleGlobalConfiguration;
|
|
+import org.galemc.gale.executor.thread.OriginalServerThread;
|
|
import org.galemc.gale.util.CPUCoresEstimation;
|
|
import org.slf4j.Logger;
|
|
|
|
@@ -82,7 +83,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
|
|
private final TextFilterClient textFilterClient;
|
|
|
|
// CraftBukkit start - Signature changed
|
|
- public DedicatedServer(joptsimple.OptionSet options, WorldLoader.DataLoadContext worldLoader, Thread thread, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PackRepository resourcepackrepository, WorldStem worldstem, DedicatedServerSettings dedicatedserversettings, DataFixer datafixer, Services services, ChunkProgressListenerFactory worldloadlistenerfactory) {
|
|
+ public DedicatedServer(joptsimple.OptionSet options, WorldLoader.DataLoadContext worldLoader, OriginalServerThread thread, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PackRepository resourcepackrepository, WorldStem worldstem, DedicatedServerSettings dedicatedserversettings, DataFixer datafixer, Services services, ChunkProgressListenerFactory worldloadlistenerfactory) { // Gale - base thread pool
|
|
super(options, worldLoader, thread, convertable_conversionsession, resourcepackrepository, worldstem, Proxy.NO_PROXY, datafixer, services, worldloadlistenerfactory);
|
|
// CraftBukkit end
|
|
this.settings = dedicatedserversettings;
|
|
diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
|
|
index 37e0b6212fec71ec9662e6be3b1e8bea487eb4a6..e7747b19685fd943d7fbefbfef656f8bb7c359f1 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
|
|
@@ -22,6 +22,8 @@ import java.io.Writer;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.util.ArrayList;
|
|
+import java.util.Arrays;
|
|
+import java.util.Collections;
|
|
import java.util.Comparator;
|
|
import java.util.Iterator;
|
|
import java.util.List;
|
|
@@ -157,6 +159,8 @@ import net.minecraft.world.phys.shapes.BooleanOp;
|
|
import net.minecraft.world.phys.shapes.Shapes;
|
|
import net.minecraft.world.phys.shapes.VoxelShape;
|
|
import net.minecraft.world.ticks.LevelTicks;
|
|
+import org.galemc.gale.executor.annotation.Access;
|
|
+import org.galemc.gale.executor.annotation.thread.AnyThreadSafe;
|
|
import org.slf4j.Logger;
|
|
import org.bukkit.Bukkit;
|
|
import org.bukkit.Location;
|
|
@@ -189,6 +193,10 @@ public class ServerLevel extends Level implements WorldGenLevel {
|
|
private static final int MAX_SCHEDULED_TICKS_PER_TICK = 65536;
|
|
final List<ServerPlayer> 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..f62da01d38533818de70761c82ffb959083e0811 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 <T, R> CompletableFuture<R> filterTextPacket(T text, BiFunction<TextFilter, T, CompletableFuture<R>> filterer) {
|
|
@@ -891,13 +892,13 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
|
|
// PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel()); // Paper - run this async
|
|
// CraftBukkit start
|
|
if (this.chatSpamTickCount.addAndGet(io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.tabSpamIncrement) > io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.tabSpamLimit && !this.server.getPlayerList().isOp(this.player.getGameProfile())) { // Paper start - split and make configurable
|
|
- server.scheduleOnMain(() -> this.disconnect(Component.translatable("disconnect.spam", ArrayConstants.emptyObjectArray), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM)); // Paper - kick event cause // Gale - JettPack - reduce array allocations
|
|
+ ScheduledServerThreadTaskQueues.add(() -> this.disconnect(Component.translatable("disconnect.spam", ArrayConstants.emptyObjectArray), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM), ScheduledServerThreadTaskQueues.KICK_FOR_COMMAND_PACKET_SPAM_TASK_MAX_DELAY); // Paper - kick event cause // Gale - JettPack - reduce array allocations // Gale - base thread pool
|
|
return;
|
|
}
|
|
// Paper start
|
|
String str = packet.getCommand(); int index = -1;
|
|
if (str.length() > 64 && ((index = str.indexOf(' ')) == -1 || index >= 64)) {
|
|
- server.scheduleOnMain(() -> this.disconnect(Component.translatable("disconnect.spam", ArrayConstants.emptyObjectArray), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM)); // Paper - kick event cause // Gale - JettPack - reduce array allocations
|
|
+ ScheduledServerThreadTaskQueues.add(() -> this.disconnect(Component.translatable("disconnect.spam", ArrayConstants.emptyObjectArray), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM), ScheduledServerThreadTaskQueues.KICK_FOR_COMMAND_PACKET_SPAM_TASK_MAX_DELAY); // Paper - kick event cause // Gale - JettPack - reduce array allocations // Gale - base thread pool
|
|
return;
|
|
}
|
|
// Paper end
|
|
@@ -922,7 +923,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
|
|
if (!event.isHandled()) {
|
|
if (!event.isCancelled()) {
|
|
|
|
- this.server.scheduleOnMain(() -> { // This needs to be on main
|
|
+ ScheduledServerThreadTaskQueues.add(() -> { // This needs to be on main // Gale - base thread pool
|
|
ParseResults<CommandSourceStack> parseresults = this.server.getCommands().getDispatcher().parse(stringreader, this.player.createCommandSourceStack());
|
|
|
|
this.server.getCommands().getDispatcher().getCompletionSuggestions(parseresults).thenAccept((suggestions) -> {
|
|
@@ -933,7 +934,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
|
|
this.connection.send(new ClientboundCommandSuggestionsPacket(packet.getId(), suggestEvent.getSuggestions()));
|
|
// Paper end - Brigadier API
|
|
});
|
|
- });
|
|
+ }, ScheduledServerThreadTaskQueues.SEND_COMMAND_COMPLETION_SUGGESTIONS_TASK_MAX_DELAY); // Gale - base thread pool
|
|
}
|
|
} else if (!completions.isEmpty()) {
|
|
final com.mojang.brigadier.suggestion.SuggestionsBuilder builder0 = new com.mojang.brigadier.suggestion.SuggestionsBuilder(command, stringreader.getTotalLength());
|
|
@@ -1247,7 +1248,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
|
|
int byteLength = testString.getBytes(java.nio.charset.StandardCharsets.UTF_8).length;
|
|
if (byteLength > 256 * 4) {
|
|
ServerGamePacketListenerImpl.LOGGER.warn(this.player.getScoreboardName() + " tried to send a book with with a page too large!");
|
|
- server.scheduleOnMain(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause
|
|
+ ScheduledServerThreadTaskQueues.add(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION), ScheduledServerThreadTaskQueues.KICK_FOR_BOOK_TOO_LARGE_PACKET_TASK_MAX_DELAY); // Paper - kick event cause // Gale - base thread pool
|
|
return;
|
|
}
|
|
byteTotal += byteLength;
|
|
@@ -1270,14 +1271,14 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
|
|
|
|
if (byteTotal > byteAllowed) {
|
|
ServerGamePacketListenerImpl.LOGGER.warn(this.player.getScoreboardName() + " tried to send too large of a book. Book Size: " + byteTotal + " - Allowed: "+ byteAllowed + " - Pages: " + pageList.size());
|
|
- server.scheduleOnMain(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause
|
|
+ ScheduledServerThreadTaskQueues.add(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION), ScheduledServerThreadTaskQueues.KICK_FOR_BOOK_TOO_LARGE_PACKET_TASK_MAX_DELAY); // Paper - kick event cause // Gale - base thread pool
|
|
return;
|
|
}
|
|
}
|
|
// Paper end
|
|
// CraftBukkit start
|
|
if (this.lastBookTick + 20 > MinecraftServer.currentTick) {
|
|
- server.scheduleOnMain(() -> this.disconnect("Book edited too quickly!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause // Paper - Also ensure this is called on main
|
|
+ ScheduledServerThreadTaskQueues.add(() -> this.disconnect("Book edited too quickly!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION), ScheduledServerThreadTaskQueues.KICK_FOR_EDITING_BOOK_TOO_QUICKLY_TASK_MAX_DELAY); // Paper - kick event cause // Paper - Also ensure this is called on main // Gale - base thread pool
|
|
return;
|
|
}
|
|
this.lastBookTick = MinecraftServer.currentTick;
|
|
@@ -2081,10 +2082,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
|
|
public void handleTeleportToEntityPacket(ServerboundTeleportToEntityPacket packet) {
|
|
PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
|
|
if (this.player.isSpectator()) {
|
|
- Iterator iterator = this.server.getAllLevels().iterator();
|
|
-
|
|
- while (iterator.hasNext()) {
|
|
- ServerLevel worldserver = (ServerLevel) iterator.next();
|
|
+ for (ServerLevel worldserver : this.server.getAllLevelsArray()) { // Gale - base thread pool - optimize server levels
|
|
Entity entity = packet.getEntity(worldserver);
|
|
|
|
if (entity != null) {
|
|
@@ -2233,9 +2231,9 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
|
|
}
|
|
// CraftBukkit end
|
|
if (ServerGamePacketListenerImpl.isChatMessageIllegal(packet.message())) {
|
|
- this.server.scheduleOnMain(() -> { // Paper - push to main for event firing
|
|
+ ScheduledServerThreadTaskQueues.add(() -> { // Paper - push to main for event firing // Gale - base thread pool
|
|
this.disconnect(Component.translatable("multiplayer.disconnect.illegal_characters"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_CHARACTERS); // Paper - add cause
|
|
- }); // Paper - push to main for event firing
|
|
+ }, ScheduledServerThreadTaskQueues.KICK_FOR_ILLEGAL_CHARACTERS_IN_CHAT_PACKET_TASK_MAX_DELAY); // Paper - push to main for event firing // Gale - base thread pool
|
|
} else {
|
|
Optional<LastSeenMessages> optional = this.tryHandleChat(packet.message(), packet.timeStamp(), packet.lastSeenMessages());
|
|
|
|
@@ -2269,9 +2267,9 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
|
|
@Override
|
|
public void handleChatCommand(ServerboundChatCommandPacket packet) {
|
|
if (ServerGamePacketListenerImpl.isChatMessageIllegal(packet.command())) {
|
|
- this.server.scheduleOnMain(() -> { // Paper - push to main for event firing
|
|
+ ScheduledServerThreadTaskQueues.add(() -> { // Paper - push to main for event firing // Gale - base thread pool
|
|
this.disconnect(Component.translatable("multiplayer.disconnect.illegal_characters"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_CHARACTERS); // Paper
|
|
- }); // Paper - push to main for event firing
|
|
+ }, ScheduledServerThreadTaskQueues.KICK_FOR_ILLEGAL_CHARACTERS_IN_CHAT_PACKET_TASK_MAX_DELAY); // Paper - push to main for event firing // Gale - base thread pool
|
|
} else {
|
|
Optional<LastSeenMessages> optional = this.tryHandleChat(packet.command(), packet.timeStamp(), packet.lastSeenMessages());
|
|
|
|
@@ -2353,9 +2351,9 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
|
|
private Optional<LastSeenMessages> 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 +3288,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
|
|
// Paper start
|
|
if (!org.bukkit.Bukkit.isPrimaryThread()) {
|
|
if (recipeSpamPackets.addAndGet(io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.recipeSpamIncrement) > io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.recipeSpamLimit) {
|
|
- server.scheduleOnMain(() -> this.disconnect(net.minecraft.network.chat.Component.translatable("disconnect.spam", ArrayConstants.emptyObjectArray), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM)); // Paper - kick event cause // Gale - JettPack - reduce array allocations
|
|
+ ScheduledServerThreadTaskQueues.add(() -> this.disconnect(net.minecraft.network.chat.Component.translatable("disconnect.spam", ArrayConstants.emptyObjectArray), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM), ScheduledServerThreadTaskQueues.KICK_FOR_RECIPE_PACKET_SPAM_TASK_MAX_DELAY); // Paper - kick event cause // Gale - JettPack - reduce array allocations // Gale - base thread pool
|
|
return;
|
|
}
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
|
|
index 6f139e6cbb61bfb2be9b8b886bec7cddbb2c8993..0cbef825129b173a5244a195ea68444c216c0b1b 100644
|
|
--- a/src/main/java/net/minecraft/server/players/PlayerList.java
|
|
+++ b/src/main/java/net/minecraft/server/players/PlayerList.java
|
|
@@ -15,7 +15,6 @@ import java.net.SocketAddress;
|
|
import java.nio.file.Path;
|
|
import java.text.SimpleDateFormat;
|
|
import java.time.Instant;
|
|
-import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Collection;
|
|
import java.util.EnumSet;
|
|
@@ -105,10 +104,10 @@ import net.minecraft.world.scores.PlayerTeam;
|
|
import net.minecraft.world.scores.Scoreboard; // Paper
|
|
import net.minecraft.world.scores.Team;
|
|
import org.galemc.gale.configuration.GaleGlobalConfiguration;
|
|
+import org.galemc.gale.executor.queue.ScheduledServerThreadTaskQueues;
|
|
import org.slf4j.Logger;
|
|
|
|
// CraftBukkit start
|
|
-import java.util.stream.Collectors;
|
|
import net.minecraft.server.dedicated.DedicatedServer;
|
|
import net.minecraft.server.level.ServerLevel;
|
|
import net.minecraft.server.level.ServerPlayer;
|
|
@@ -255,7 +254,7 @@ public abstract class PlayerList {
|
|
|
|
// Gale start - MultiPaper - do not place player in world if kicked before being spawned in
|
|
if (!connection.isConnected() || player.quitReason != null) {
|
|
- pendingPlayers.remove(player.getUUID(), player);
|
|
+ /*pendingPlayers.remove(player.getUUID(), player);*/ // Gale - base thread pool - this patch was removed from Paper but might be useful later
|
|
return;
|
|
}
|
|
// Gale end - MultiPaper - do not place player in world if kicked before being spawned in
|
|
@@ -296,6 +295,58 @@ public abstract class PlayerList {
|
|
player.getRecipeBook().sendInitialRecipeBook(player);
|
|
this.updateEntireScoreboard(worldserver1.getScoreboard(), player);
|
|
this.server.invalidateStatus();
|
|
+/* // Gale - base thread pool - this patch was removed from Paper but might be useful later
|
|
+ // Paper start - async load spawn in chunk
|
|
+ ServerLevel finalWorldserver = worldserver1;
|
|
+ finalWorldserver.pendingLogin.add(player);
|
|
+ int chunkX = loc.getBlockX() >> 4;
|
|
+ int chunkZ = loc.getBlockZ() >> 4;
|
|
+ final net.minecraft.world.level.ChunkPos pos = new net.minecraft.world.level.ChunkPos(chunkX, chunkZ);
|
|
+ net.minecraft.server.level.ChunkMap playerChunkMap = worldserver1.getChunkSource().chunkMap;
|
|
+ net.minecraft.server.level.DistanceManager distanceManager = playerChunkMap.distanceManager;
|
|
+ io.papermc.paper.chunk.system.ChunkSystem.scheduleTickingState(
|
|
+ worldserver1, chunkX, chunkZ, net.minecraft.server.level.ChunkHolder.FullChunkStatus.ENTITY_TICKING, true,
|
|
+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHEST,
|
|
+ (chunk) -> {
|
|
+ ScheduledServerThreadTaskQueues.add(() -> { // Gale - base thread pool
|
|
+ try {
|
|
+ if (!playerconnection.connection.isConnected()) {
|
|
+ return;
|
|
+ }
|
|
+ PlayerList.this.postChunkLoadJoin(
|
|
+ player, finalWorldserver, connection, playerconnection,
|
|
+ nbttagcompound, s1, lastKnownName
|
|
+ );
|
|
+ distanceManager.addTicket(net.minecraft.server.level.TicketType.LOGIN, pos, 31, pos.toLong());
|
|
+ } finally {
|
|
+ finalWorldserver.pendingLogin.remove(player);
|
|
+ }
|
|
+ }, ScheduledServerThreadTaskQueues.POST_CHUNK_LOAD_JOIN_TASK_MAX_DELAY); // Gale - base thread pool
|
|
+ }
|
|
+ );
|
|
+ }
|
|
+
|
|
+ public ServerPlayer getActivePlayer(UUID uuid) {
|
|
+ ServerPlayer player = this.playersByUUID.get(uuid);
|
|
+ return player != null ? player : pendingPlayers.get(uuid);
|
|
+ }
|
|
+
|
|
+ void disconnectPendingPlayer(ServerPlayer entityplayer) {
|
|
+ Component msg = Component.translatable("multiplayer.disconnect.duplicate_login");
|
|
+ entityplayer.networkManager.send(new net.minecraft.network.protocol.game.ClientboundDisconnectPacket(msg), net.minecraft.network.PacketSendListener.thenRun(() -> {
|
|
+ entityplayer.networkManager.disconnect(msg);
|
|
+ entityplayer.networkManager = null;
|
|
+ }));
|
|
+ }
|
|
+
|
|
+ private void postChunkLoadJoin(ServerPlayer player, ServerLevel worldserver1, Connection networkmanager, ServerGamePacketListenerImpl playerconnection, CompoundTag nbttagcompound, String s1, String s) {
|
|
+ pendingPlayers.remove(player.getUUID(), player);
|
|
+ if (!networkmanager.isConnected()) {
|
|
+ return;
|
|
+ }
|
|
+ player.didPlayerJoinEvent = true;
|
|
+ // Paper end
|
|
+*/ // Gale - base thread pool - this patch was removed from Paper but might be useful later
|
|
MutableComponent ichatmutablecomponent;
|
|
|
|
if (player.getGameProfile().getName().equalsIgnoreCase(s)) {
|
|
@@ -1523,10 +1574,8 @@ public abstract class PlayerList {
|
|
public void setViewDistance(int viewDistance) {
|
|
this.viewDistance = viewDistance;
|
|
//this.broadcastAll(new ClientboundSetChunkCacheRadiusPacket(viewDistance)); // Paper - move into setViewDistance
|
|
- Iterator iterator = this.server.getAllLevels().iterator();
|
|
|
|
- while (iterator.hasNext()) {
|
|
- ServerLevel worldserver = (ServerLevel) iterator.next();
|
|
+ for (ServerLevel worldserver : this.server.getAllLevelsArray()) { // Gale - base thread pool - optimize server levels
|
|
|
|
if (worldserver != null) {
|
|
worldserver.getChunkSource().setViewDistance(viewDistance);
|
|
@@ -1538,10 +1587,8 @@ public abstract class PlayerList {
|
|
public void setSimulationDistance(int simulationDistance) {
|
|
this.simulationDistance = simulationDistance;
|
|
//this.broadcastAll(new ClientboundSetSimulationDistancePacket(simulationDistance)); // Paper - handled by playerchunkloader
|
|
- Iterator iterator = this.server.getAllLevels().iterator();
|
|
|
|
- while (iterator.hasNext()) {
|
|
- ServerLevel worldserver = (ServerLevel) iterator.next();
|
|
+ for (ServerLevel worldserver : this.server.getAllLevelsArray()) { // Gale - base thread pool - optimize server levels
|
|
|
|
if (worldserver != null) {
|
|
worldserver.getChunkSource().setSimulationDistance(simulationDistance);
|
|
diff --git a/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java b/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java
|
|
index 83701fbfaa56a232593ee8f11a3afb8941238bfa..392e7b4a89669f16b32043b65b69e6593d17f10e 100644
|
|
--- a/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java
|
|
+++ b/src/main/java/net/minecraft/util/thread/BlockableEventLoop.java
|
|
@@ -6,17 +6,18 @@ import com.mojang.logging.LogUtils;
|
|
import java.util.List;
|
|
import java.util.Queue;
|
|
import java.util.concurrent.CompletableFuture;
|
|
-import java.util.concurrent.Executor;
|
|
import java.util.concurrent.locks.LockSupport;
|
|
import java.util.function.BooleanSupplier;
|
|
import java.util.function.Supplier;
|
|
+
|
|
import net.minecraft.util.profiling.metrics.MetricCategory;
|
|
import net.minecraft.util.profiling.metrics.MetricSampler;
|
|
import net.minecraft.util.profiling.metrics.MetricsRegistry;
|
|
import net.minecraft.util.profiling.metrics.ProfilerMeasured;
|
|
+import org.galemc.gale.executor.AbstractBlockableEventLoop;
|
|
import org.slf4j.Logger;
|
|
|
|
-public abstract class BlockableEventLoop<R extends Runnable> implements ProfilerMeasured, ProcessorHandle<R>, Executor {
|
|
+public abstract class BlockableEventLoop<R extends Runnable> implements ProfilerMeasured, ProcessorHandle<R>, AbstractBlockableEventLoop { // Gale - base thread pool
|
|
private final String name;
|
|
private static final Logger LOGGER = LogUtils.getLogger();
|
|
private final Queue<R> pendingRunnables = Queues.newConcurrentLinkedQueue();
|
|
@@ -31,6 +32,7 @@ public abstract class BlockableEventLoop<R extends Runnable> implements Profiler
|
|
|
|
protected abstract boolean shouldRun(R task);
|
|
|
|
+ @Override // Gale - base thread pool
|
|
public boolean isSameThread() {
|
|
return Thread.currentThread() == this.getRunningThread();
|
|
}
|
|
@@ -45,6 +47,12 @@ public abstract class BlockableEventLoop<R extends Runnable> implements Profiler
|
|
return this.pendingRunnables.size();
|
|
}
|
|
|
|
+ // Gale start - base thread 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<R extends Runnable> 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<Chunk> ret = new java.util.concurrent.CompletableFuture<>();
|
|
|
|
io.papermc.paper.chunk.system.ChunkSystem.scheduleChunkLoad(this.getHandle(), x, z, gen, ChunkStatus.FULL, true, priority, (c) -> {
|
|
- net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> {
|
|
+ ScheduledServerThreadTaskQueues.add(() -> { // Gale - base thread 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 f7bedd5cbe9b48ac94f8cc228a17c8a54db7d7e9..129ac4f70f2e7f176517af1e46ea36c5144473a0 100644
|
|
--- a/src/main/java/org/galemc/gale/concurrent/Mutex.java
|
|
+++ b/src/main/java/org/galemc/gale/concurrent/Mutex.java
|
|
@@ -20,7 +20,7 @@ import java.util.concurrent.locks.Lock;
|
|
* <br>
|
|
* This interface extends {@link AutoCloseable}, where {@link #close()} calls {@link #release()}.
|
|
*
|
|
- * @author Martijn Muijsers
|
|
+ * @author Martijn Muijsers under AGPL-3.0
|
|
*/
|
|
@AnyThreadSafe
|
|
public interface Mutex extends CheckableLock, AutoCloseable {
|
|
diff --git a/src/main/java/org/galemc/gale/concurrent/SemaphoreMutex.java b/src/main/java/org/galemc/gale/concurrent/SemaphoreMutex.java
|
|
index c310264afea6c81ec575bdf6aa5495ccb34d7ae4..d31293a2a2151bc9fbdc6eb2175045b429fb4461 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<GaleGlobalConfiguration,
|
|
try {
|
|
this.initializeGlobalConfiguration(reloader(this.globalConfigClass, GaleGlobalConfiguration.get()));
|
|
this.initializeWorldDefaultsConfiguration();
|
|
- for (ServerLevel level : server.getAllLevels()) {
|
|
+ for (ServerLevel level : server.getAllLevelsArray()) { // Gale - base thread pool - optimize server levels
|
|
this.createWorldConfig(PaperConfigurations.createWorldContextMap(level), reloader(this.worldConfigClass, level.galeConfig()));
|
|
}
|
|
} catch (Exception ex) {
|
|
diff --git a/src/main/java/org/galemc/gale/configuration/GaleGlobalConfiguration.java b/src/main/java/org/galemc/gale/configuration/GaleGlobalConfiguration.java
|
|
index 7a3111603c75105769cf0fc3ff3c5ee6d45b57e5..39c4fe8fdd806b4b5bc3cb2dfdde9a29b11b386e 100644
|
|
--- a/src/main/java/org/galemc/gale/configuration/GaleGlobalConfiguration.java
|
|
+++ b/src/main/java/org/galemc/gale/configuration/GaleGlobalConfiguration.java
|
|
@@ -2,11 +2,14 @@
|
|
|
|
package org.galemc.gale.configuration;
|
|
|
|
+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
|
|
import io.papermc.paper.configuration.Configuration;
|
|
import io.papermc.paper.configuration.ConfigurationPart;
|
|
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
|
|
+import org.galemc.gale.executor.queue.ScheduledServerThreadTaskQueues;
|
|
import org.spongepowered.configurate.objectmapping.meta.Setting;
|
|
|
|
+import java.util.Arrays;
|
|
import java.util.Locale;
|
|
import java.util.function.Consumer;
|
|
|
|
@@ -59,6 +62,223 @@ public class GaleGlobalConfiguration extends ConfigurationPart {
|
|
}
|
|
// Gale end - Pufferfish - SIMD support
|
|
|
|
+ // Gale start - base thread pool
|
|
+ public MainThreadTaskMaxDelay mainThreadTaskMaxDelay;
|
|
+ public class MainThreadTaskMaxDelay extends ConfigurationPart.Post {
|
|
+
|
|
+ /**
|
|
+ * The default maximum delay for tasks.
|
|
+ * Given in ticks.
|
|
+ * Any value < 0 uses the vanilla maximum delay for tasks, which is currently 2.
|
|
+ * <ul>
|
|
+ * <li><i>Default</i>: -1</li>
|
|
+ * <li><i>Vanilla</i>: -1</li>
|
|
+ * </ul>
|
|
+ */
|
|
+ @Setting("default")
|
|
+ public int defaultValue = -1;
|
|
+
|
|
+ /**
|
|
+ * The default maximum delay for completing a {@link java.util.concurrent.CompletableFuture}
|
|
+ * for a chunk load, after the chunk has already finished loading.
|
|
+ * Given in ticks.
|
|
+ * Any value < 0 uses {@link #defaultValue}.
|
|
+ * <ul>
|
|
+ * <li><i>Default</i>: 0</li>
|
|
+ * <li><i>Vanilla</i>: -1</li>
|
|
+ * </ul>
|
|
+ */
|
|
+ public int completeChunkFuture = 0;
|
|
+
|
|
+ /**
|
|
+ * The default maximum delay for completing the steps needed to take when a player is joining and the
|
|
+ * necessary chunk has been loaded.
|
|
+ * Given in ticks.
|
|
+ * Any value < 0 uses {@link #defaultValue}.
|
|
+ * <ul>
|
|
+ * <li><i>Default</i>: 19</li>
|
|
+ * <li><i>Vanilla</i>: -1</li>
|
|
+ * </ul>
|
|
+ */
|
|
+ public int postChunkLoadJoin = 19;
|
|
+
|
|
+ /**
|
|
+ * The default maximum delay for chunk packets to be modified for anti-xray.
|
|
+ * Given in ticks.
|
|
+ * Any value < 0 uses {@link #defaultValue}.
|
|
+ * <ul>
|
|
+ * <li><i>Default</i>: 19</li>
|
|
+ * <li><i>Vanilla</i>: -1</li>
|
|
+ * </ul>
|
|
+ */
|
|
+ public int antiXrayModifyBlocks = 19;
|
|
+
|
|
+ /**
|
|
+ * The default maximum delay for entities to be teleported when a teleport is started asynchronously.
|
|
+ * Given in ticks.
|
|
+ * Any value < 0 uses {@link #defaultValue}.
|
|
+ * <ul>
|
|
+ * <li><i>Default</i>: -1</li>
|
|
+ * <li><i>Vanilla</i>: -1</li>
|
|
+ * </ul>
|
|
+ */
|
|
+ public int teleportAsync = -1;
|
|
+
|
|
+ /**
|
|
+ * The default maximum delay for command completion suggestions to be sent to the player.
|
|
+ * Any value < 0 uses {@link #defaultValue}.
|
|
+ * <ul>
|
|
+ * <li><i>Default</i>: 9</li>
|
|
+ * <li><i>Vanilla</i>: -1</li>
|
|
+ * </ul>
|
|
+ */
|
|
+ public int sendCommandCompletionSuggestions = 9;
|
|
+
|
|
+ /**
|
|
+ * The default maximum delay for players to get kicked for command packet spam.
|
|
+ * Given in ticks.
|
|
+ * Any value < 0 uses {@link #defaultValue}.
|
|
+ * <ul>
|
|
+ * <li><i>Default</i>: 0</li>
|
|
+ * <li><i>Vanilla</i>: -1</li>
|
|
+ * </ul>
|
|
+ */
|
|
+ public int kickForCommandPacketSpam = 0;
|
|
+
|
|
+ /**
|
|
+ * The default maximum delay for players to get kicked for place-recipe packet spam.
|
|
+ * Given in ticks.
|
|
+ * Any value < 0 uses {@link #defaultValue}.
|
|
+ * <ul>
|
|
+ * <li><i>Default</i>: 0</li>
|
|
+ * <li><i>Vanilla</i>: -1</li>
|
|
+ * </ul>
|
|
+ */
|
|
+ public int kickForRecipePacketSpam = 0;
|
|
+
|
|
+ /**
|
|
+ * The default maximum delay for players to get kicked for sending invalid packets trying to
|
|
+ * send book content that is too large, which usually indicates they are attempting to abuse an exploit.
|
|
+ * Given in ticks.
|
|
+ * Any value < 0 uses {@link #defaultValue}.
|
|
+ * <ul>
|
|
+ * <li><i>Default</i>: -1</li>
|
|
+ * <li><i>Vanilla</i>: -1</li>
|
|
+ * </ul>
|
|
+ */
|
|
+ public int kickForBookTooLargePacket = -1;
|
|
+
|
|
+ /**
|
|
+ * The default maximum delay for players to get kicked for editing a book too quickly.
|
|
+ * Given in ticks.
|
|
+ * Any value < 0 uses {@link #defaultValue}.
|
|
+ * <ul>
|
|
+ * <li><i>Default</i>: -1</li>
|
|
+ * <li><i>Vanilla</i>: -1</li>
|
|
+ * </ul>
|
|
+ */
|
|
+ public int kickForEditingBookTooQuickly = -1;
|
|
+
|
|
+ /**
|
|
+ * The default maximum delay for players to get kicked for sending a chat packet with illegal characters.
|
|
+ * Given in ticks.
|
|
+ * Any value < 0 uses {@link #defaultValue}.
|
|
+ * <ul>
|
|
+ * <li><i>Default</i>: -1</li>
|
|
+ * <li><i>Vanilla</i>: -1</li>
|
|
+ * </ul>
|
|
+ */
|
|
+ public int kickForIllegalCharactersInChatPacket = -1;
|
|
+
|
|
+ /**
|
|
+ * The default maximum delay for players to get kicked for sending an out-of-order chat packet.
|
|
+ * Given in ticks.
|
|
+ * Any value < 0 uses {@link #defaultValue}.
|
|
+ * <ul>
|
|
+ * <li><i>Default</i>: -1</li>
|
|
+ * <li><i>Vanilla</i>: -1</li>
|
|
+ * </ul>
|
|
+ */
|
|
+ public int kickForOutOfOrderChatPacket = -1;
|
|
+
|
|
+ /**
|
|
+ * The default maximum delay for handling player disconnects.
|
|
+ * Any value < 0 uses {@link #defaultValue}.
|
|
+ * <ul>
|
|
+ * <li><i>Default</i>: -1</li>
|
|
+ * <li><i>Vanilla</i>: -1</li>
|
|
+ * </ul>
|
|
+ */
|
|
+ public int handleDisconnect = -1;
|
|
+
|
|
+ @Override
|
|
+ public void postProcess() {
|
|
+ while (!ScheduledServerThreadTaskQueues.writeLock.tryLock());
|
|
+ try {
|
|
+ // Update the values in MinecraftServerBlockableEventLoop for quick access
|
|
+ ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY = this.defaultValue >= 0 ? this.defaultValue : 2;
|
|
+ ScheduledServerThreadTaskQueues.COMPLETE_CHUNK_FUTURE_TASK_MAX_DELAY = this.completeChunkFuture >= 0 ? this.completeChunkFuture : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY;
|
|
+ ScheduledServerThreadTaskQueues.POST_CHUNK_LOAD_JOIN_TASK_MAX_DELAY = this.postChunkLoadJoin >= 0 ? this.postChunkLoadJoin : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY;
|
|
+ ScheduledServerThreadTaskQueues.ANTI_XRAY_MODIFY_BLOCKS_TASK_MAX_DELAY = this.antiXrayModifyBlocks >= 0 ? this.antiXrayModifyBlocks : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY;
|
|
+ ScheduledServerThreadTaskQueues.TELEPORT_ASYNC_TASK_MAX_DELAY = this.teleportAsync >= 0 ? this.teleportAsync : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY;
|
|
+ ScheduledServerThreadTaskQueues.SEND_COMMAND_COMPLETION_SUGGESTIONS_TASK_MAX_DELAY = this.sendCommandCompletionSuggestions >= 0 ? this.sendCommandCompletionSuggestions : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY;
|
|
+ ScheduledServerThreadTaskQueues.KICK_FOR_COMMAND_PACKET_SPAM_TASK_MAX_DELAY = this.kickForCommandPacketSpam >= 0 ? this.kickForCommandPacketSpam : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY;
|
|
+ ScheduledServerThreadTaskQueues.KICK_FOR_RECIPE_PACKET_SPAM_TASK_MAX_DELAY = this.kickForRecipePacketSpam >= 0 ? this.kickForRecipePacketSpam : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY;
|
|
+ ScheduledServerThreadTaskQueues.KICK_FOR_BOOK_TOO_LARGE_PACKET_TASK_MAX_DELAY = this.kickForBookTooLargePacket >= 0 ? this.kickForBookTooLargePacket : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY;
|
|
+ ScheduledServerThreadTaskQueues.KICK_FOR_EDITING_BOOK_TOO_QUICKLY_TASK_MAX_DELAY = this.kickForEditingBookTooQuickly >= 0 ? this.kickForEditingBookTooQuickly : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY;
|
|
+ ScheduledServerThreadTaskQueues.KICK_FOR_ILLEGAL_CHARACTERS_IN_CHAT_PACKET_TASK_MAX_DELAY = this.kickForIllegalCharactersInChatPacket >= 0 ? this.kickForIllegalCharactersInChatPacket : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY;
|
|
+ ScheduledServerThreadTaskQueues.KICK_FOR_OUT_OF_ORDER_CHAT_PACKET_TASK_MAX_DELAY = this.kickForOutOfOrderChatPacket >= 0 ? this.kickForOutOfOrderChatPacket : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY;
|
|
+ ScheduledServerThreadTaskQueues.HANDLE_DISCONNECT_TASK_MAX_DELAY = this.handleDisconnect >= 0 ? this.handleDisconnect : ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY;
|
|
+ // Change the length of the pendingRunnables array of queues
|
|
+ int maxDelay = 0;
|
|
+ for (int delay : new int[]{
|
|
+ ScheduledServerThreadTaskQueues.DEFAULT_TASK_MAX_DELAY,
|
|
+ ScheduledServerThreadTaskQueues.COMPLETE_CHUNK_FUTURE_TASK_MAX_DELAY,
|
|
+ ScheduledServerThreadTaskQueues.POST_CHUNK_LOAD_JOIN_TASK_MAX_DELAY,
|
|
+ ScheduledServerThreadTaskQueues.ANTI_XRAY_MODIFY_BLOCKS_TASK_MAX_DELAY,
|
|
+ ScheduledServerThreadTaskQueues.TELEPORT_ASYNC_TASK_MAX_DELAY,
|
|
+ ScheduledServerThreadTaskQueues.SEND_COMMAND_COMPLETION_SUGGESTIONS_TASK_MAX_DELAY,
|
|
+ ScheduledServerThreadTaskQueues.KICK_FOR_COMMAND_PACKET_SPAM_TASK_MAX_DELAY,
|
|
+ ScheduledServerThreadTaskQueues.KICK_FOR_RECIPE_PACKET_SPAM_TASK_MAX_DELAY,
|
|
+ ScheduledServerThreadTaskQueues.KICK_FOR_BOOK_TOO_LARGE_PACKET_TASK_MAX_DELAY,
|
|
+ ScheduledServerThreadTaskQueues.KICK_FOR_EDITING_BOOK_TOO_QUICKLY_TASK_MAX_DELAY,
|
|
+ ScheduledServerThreadTaskQueues.KICK_FOR_ILLEGAL_CHARACTERS_IN_CHAT_PACKET_TASK_MAX_DELAY,
|
|
+ ScheduledServerThreadTaskQueues.KICK_FOR_OUT_OF_ORDER_CHAT_PACKET_TASK_MAX_DELAY,
|
|
+ ScheduledServerThreadTaskQueues.HANDLE_DISCONNECT_TASK_MAX_DELAY
|
|
+ }) {
|
|
+ if (delay > maxDelay) {
|
|
+ maxDelay = delay;
|
|
+ }
|
|
+ }
|
|
+ int newPendingRunnablesLength = maxDelay + 1;
|
|
+ int oldPendingRunnablesLength = ScheduledServerThreadTaskQueues.queues.length;
|
|
+ if (oldPendingRunnablesLength != newPendingRunnablesLength) {
|
|
+ if (oldPendingRunnablesLength > newPendingRunnablesLength) {
|
|
+ // Move all tasks in queues that will be removed to the last queue
|
|
+ for (int i = newPendingRunnablesLength + 1; i < ScheduledServerThreadTaskQueues.queues.length; i++) {
|
|
+ ScheduledServerThreadTaskQueues.queues[maxDelay].addAll(ScheduledServerThreadTaskQueues.queues[i]);
|
|
+ }
|
|
+ // Update the first queue with elements index
|
|
+ if (ScheduledServerThreadTaskQueues.firstQueueWithPotentialTasksIndex >= newPendingRunnablesLength) {
|
|
+ ScheduledServerThreadTaskQueues.firstQueueWithPotentialTasksIndex = maxDelay;
|
|
+ }
|
|
+ }
|
|
+ ScheduledServerThreadTaskQueues.queues = Arrays.copyOf(ScheduledServerThreadTaskQueues.queues, newPendingRunnablesLength);
|
|
+ if (newPendingRunnablesLength > oldPendingRunnablesLength) {
|
|
+ // Create new queues
|
|
+ for (int i = oldPendingRunnablesLength; i < newPendingRunnablesLength; i++) {
|
|
+ ScheduledServerThreadTaskQueues.queues[i] = new MultiThreadedQueue<>();
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ } finally {
|
|
+ ScheduledServerThreadTaskQueues.writeLock.unlock();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ }
|
|
+ // Gale end - base thread 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<Runnable>, AbstractBlockableEventLoop {
|
|
+
|
|
+ private static final String NAME = "Server";
|
|
+ private static final Logger LOGGER = LogUtils.getLogger();
|
|
+
|
|
+ public static volatile int blockingCount;
|
|
+ private static volatile int reentrantCount;
|
|
+
|
|
+ public static boolean scheduleExecutables() {
|
|
+ return (reentrantCount != 0 || Thread.currentThread() != ServerThread.getInstance()) && !MinecraftServer.SERVER.isStopped();
|
|
+ }
|
|
+
|
|
+ protected boolean runningTask() {
|
|
+ return reentrantCount != 0;
|
|
+ }
|
|
+
|
|
+ public <V> CompletableFuture<V> submit(Supplier<V> task) {
|
|
+ return scheduleExecutables() ? CompletableFuture.supplyAsync(task, this) : CompletableFuture.completedFuture(task.get());
|
|
+ }
|
|
+
|
|
+ private CompletableFuture<Void> submitAsync(Runnable runnable) {
|
|
+ return CompletableFuture.supplyAsync(() -> {
|
|
+ runnable.run();
|
|
+ return null;
|
|
+ }, this);
|
|
+ }
|
|
+
|
|
+ public CompletableFuture<Void> submit(Runnable task) {
|
|
+ if (scheduleExecutables()) {
|
|
+ return this.submitAsync(task);
|
|
+ } else {
|
|
+ task.run();
|
|
+ return CompletableFuture.completedFuture(null);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public void executeBlocking(Runnable runnable) {
|
|
+ if (Thread.currentThread() != ServerThread.getInstance()) {
|
|
+ this.submitAsync(runnable).join();
|
|
+ } else {
|
|
+ runnable.run();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * @deprecated Use {@link ScheduledServerThreadTaskQueues#add(Runnable, int)} instead:
|
|
+ * do not rely on {@link ScheduledServerThreadTaskQueues#DEFAULT_TASK_MAX_DELAY}.
|
|
+ */
|
|
+ @Deprecated
|
|
+ @Override
|
|
+ public void tell(@NotNull Runnable message) {
|
|
+ ScheduledServerThreadTaskQueues.add(() -> {
|
|
+ //noinspection NonAtomicOperationOnVolatileField
|
|
+ ++reentrantCount;
|
|
+ try {
|
|
+ message.run();
|
|
+ } catch (Exception var3) {
|
|
+ if (var3.getCause() instanceof ThreadDeath) throw var3; // Paper
|
|
+ LOGGER.error(LogUtils.FATAL_MARKER, "Error executing task on {}", NAME, var3);
|
|
+ } finally {
|
|
+ //noinspection NonAtomicOperationOnVolatileField
|
|
+ --reentrantCount;
|
|
+ if (MinecraftServer.isWaitingUntilNextTick) {
|
|
+ MinecraftServer.signalServerThreadIfCurrentManagedBlockStopConditionBecameTrue();
|
|
+ }
|
|
+ }
|
|
+ MinecraftServer.SERVER.executeMidTickTasks(); // Paper - execute chunk tasks mid tick
|
|
+ });
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void execute(@NotNull Runnable var1) {
|
|
+ if (scheduleExecutables()) {
|
|
+ this.tell(var1);
|
|
+ } else {
|
|
+ var1.run();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean isSameThread() {
|
|
+ return Thread.currentThread() == MinecraftServer.serverThread;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void executeIfPossible(Runnable runnable) {
|
|
+ if (MinecraftServer.SERVER.isStopped()) {
|
|
+ throw new RejectedExecutionException("Server already shutting down");
|
|
+ } else {
|
|
+ this.execute(runnable);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Runs all tasks, regardless of which tick they must be finished in, or whether there is time.
|
|
+ */
|
|
+ @ServerThreadOnly
|
|
+ protected void runAllMainThreadTasksForAllTicks() {
|
|
+ Runnable task;
|
|
+ while (true) {
|
|
+ // Force polling every tasks regardless of the tick they have to be finished by
|
|
+ MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking = false;
|
|
+ task = ScheduledServerThreadTaskQueues.poll(ServerThread.getInstance(), true);
|
|
+ if (task == null) {
|
|
+ break;
|
|
+ }
|
|
+ task.run();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Runs all tasks while there is time.
|
|
+ * Runs at least all tasks that must be finished in the current tick, regardless of whether there is time.
|
|
+ */
|
|
+ @ServerThreadOnly
|
|
+ protected void runAllTasksWithinTimeOrForCurrentTick() {
|
|
+ Runnable task;
|
|
+ while (true) {
|
|
+ /*
|
|
+ Update this value accurately: we are in 'spare time' here, we may have more time or not, and we are
|
|
+ definitely not already blocking.
|
|
+ */
|
|
+ MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking = !MinecraftServer.SERVER.haveTime();
|
|
+ task = BaseTaskQueues.anyTickScheduledServerThread.poll(ServerThread.getInstance());
|
|
+ if (task == null) {
|
|
+ break;
|
|
+ }
|
|
+ task.run();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @ServerThreadOnly
|
|
+ public void managedBlock(@NotNull BooleanSupplier stopCondition) {
|
|
+ MinecraftServer.currentManagedBlockStopCondition = stopCondition;
|
|
+ try {
|
|
+ // Check stop condition beforehand to prevent unnecessarily releasing main thread
|
|
+ MinecraftServer.currentManagedBlockStopConditionHasBecomeTrue = false;
|
|
+ if (stopCondition.getAsBoolean()) {
|
|
+ MinecraftServer.currentManagedBlockStopConditionHasBecomeTrue = true;
|
|
+ return;
|
|
+ }
|
|
+ //noinspection NonAtomicOperationOnVolatileField
|
|
+ ++blockingCount;
|
|
+ try {
|
|
+ MinecraftServer.serverThread.runTasksUntil(stopCondition, null);
|
|
+ } finally {
|
|
+ //noinspection NonAtomicOperationOnVolatileField
|
|
+ --blockingCount;
|
|
+ }
|
|
+ } finally {
|
|
+ MinecraftServer.currentManagedBlockStopCondition = null;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public @NotNull String name() {
|
|
+ return NAME;
|
|
+ }
|
|
+
|
|
+}
|
|
diff --git a/src/main/java/org/galemc/gale/executor/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.
|
|
* <br>
|
|
* 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.
|
|
* <br>
|
|
* Methods that are potentially blocking, including those annotated with {@link PotentiallyBlocking}, must never
|
|
- * be called on a {@link BaseThread}.
|
|
+ * be called on an {@link AbstractYieldingThread}.
|
|
*
|
|
* @author Martijn Muijsers under AGPL-3.0
|
|
*/
|
|
diff --git a/src/main/java/org/galemc/gale/executor/annotation/PotentiallyYielding.java b/src/main/java/org/galemc/gale/executor/annotation/PotentiallyYielding.java
|
|
index e87ee2612348fc559b21256cc7cadfc684f01f8e..7ff4e4ab43d316e319efb33b2dd365d679a58118 100644
|
|
--- a/src/main/java/org/galemc/gale/executor/annotation/PotentiallyYielding.java
|
|
+++ b/src/main/java/org/galemc/gale/executor/annotation/PotentiallyYielding.java
|
|
@@ -2,6 +2,9 @@
|
|
|
|
package org.galemc.gale.executor.annotation;
|
|
|
|
+import org.galemc.gale.executor.lock.YieldingLock;
|
|
+import org.galemc.gale.executor.thread.AbstractYieldingThread;
|
|
+
|
|
import java.lang.annotation.Documented;
|
|
import java.lang.annotation.ElementType;
|
|
import java.lang.annotation.Target;
|
|
@@ -16,6 +19,9 @@ import java.lang.annotation.Target;
|
|
* <br>
|
|
* In a method annotated with {@link PotentiallyYielding}, the only methods that can be called are those
|
|
* annotated with {@link PotentiallyYielding} or {@link YieldFree}.
|
|
+ * <br>
|
|
+ * It should be assumed that any method annotated with {@link PotentiallyYielding} is potentially blocking if used
|
|
+ * on a thread that is not a {@link AbstractYieldingThread}.
|
|
*
|
|
* @author Martijn Muijsers under AGPL-3.0
|
|
*/
|
|
diff --git a/src/main/java/org/galemc/gale/executor/annotation/thread/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}.
|
|
+ * <br>
|
|
+ * This annotation can also be used on fields or classes, similar to {@link ThreadRestricted}.
|
|
+ * <br>
|
|
+ * In a method annotated with {@link AssistThreadOnly}, fields and methods annotated with
|
|
+ * {@link AssistThreadOnly}, {@link BaseThreadOnly} or {@link AnyThreadSafe} may be used.
|
|
+ * <br>
|
|
+ * 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}.
|
|
+ * <br>
|
|
+ * This annotation can also be used on fields or classes, similar to {@link ThreadRestricted}.
|
|
+ * <br>
|
|
+ * In a method annotated with {@link BaseThreadOnly}, fields and methods annotated with
|
|
+ * {@link BaseThreadOnly} or {@link AnyThreadSafe} may be used.
|
|
+ * <br>
|
|
+ * 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}.
|
|
+ * <br>
|
|
+ * This annotation can also be used on fields, similar to {@link ThreadRestricted}.
|
|
+ * <br>
|
|
+ * In a method annotated with {@link OriginalServerThreadOnly}, fields and methods annotated with
|
|
+ * {@link OriginalServerThreadOnly}, {@link ServerThreadOnly}, {@link BaseThreadOnly}
|
|
+ * or {@link AnyThreadSafe} may be used.
|
|
+ * <br>
|
|
+ * Methods that are annotated with {@link OriginalServerThreadOnly} must never call methods that are annotated with
|
|
+ * {@link PotentiallyBlocking}.
|
|
+ *
|
|
+ * @see ThreadRestricted
|
|
+ *
|
|
+ * @author Martijn Muijsers under AGPL-3.0
|
|
+ */
|
|
+@Documented
|
|
+@Target({ElementType.METHOD, ElementType.FIELD})
|
|
+public @interface OriginalServerThreadOnly {
|
|
+
|
|
+ /**
|
|
+ * @see ThreadRestricted#fieldAccess()
|
|
+ */
|
|
+ Access value() default Access.READ_WRITE;
|
|
+
|
|
+}
|
|
diff --git a/src/main/java/org/galemc/gale/executor/annotation/thread/ServerThreadOnly.java b/src/main/java/org/galemc/gale/executor/annotation/thread/ServerThreadOnly.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..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}.
|
|
+ * <br>
|
|
+ * This annotation can also be used on fields or classes, similar to {@link ThreadRestricted}.
|
|
+ * <br>
|
|
+ * In a method annotated with {@link ServerThreadOnly}, fields and methods annotated with
|
|
+ * {@link ServerThreadOnly}, {@link BaseThreadOnly} or {@link AnyThreadSafe} may be used.
|
|
+ * <br>
|
|
+ * Methods that are annotated with {@link ServerThreadOnly} must never call methods that are annotated with
|
|
+ * {@link PotentiallyBlocking}.
|
|
+ *
|
|
+ * @see ThreadRestricted
|
|
+ *
|
|
+ * @author Martijn Muijsers under AGPL-3.0
|
|
+ */
|
|
+@Documented
|
|
+@Target({ElementType.METHOD, ElementType.FIELD})
|
|
+public @interface ServerThreadOnly {
|
|
+
|
|
+ /**
|
|
+ * @see ThreadRestricted#fieldAccess()
|
|
+ */
|
|
+ Access value() default Access.READ_WRITE;
|
|
+
|
|
+}
|
|
diff --git a/src/main/java/org/galemc/gale/executor/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..3ec790a7b19790731f70f59cc2bdb1919f26dd2d
|
|
--- /dev/null
|
|
+++ b/src/main/java/org/galemc/gale/executor/lock/YieldingLock.java
|
|
@@ -0,0 +1,163 @@
|
|
+// 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.
|
|
+ * <br>
|
|
+ * The lock is reentrant if the underlying controlled lock is.
|
|
+ * <br>
|
|
+ * The lock only be speculatively acquired from any {@link AbstractYieldingThread}.
|
|
+ * Acquiring it on a thread that is not an {@link AbstractYieldingThread} will perform regular locking
|
|
+ * on the underlying controlled lock, which typically blocks the thread.
|
|
+ * <br>
|
|
+ * A thread cannot acquire a {@link YieldingLock} when it already owns one.
|
|
+ *
|
|
+ * @author Martijn Muijsers under AGPL-3.0
|
|
+ */
|
|
+@AnyThreadSafe
|
|
+public abstract class YieldingLock implements CheckableLock {
|
|
+
|
|
+ private final Lock innerLock;
|
|
+
|
|
+ /**
|
|
+ * The same value as {@link #lock}, or null if it is not an instance of {@link CheckableLock}.
|
|
+ */
|
|
+ private final @Nullable CheckableLock innerCheckableLock;
|
|
+
|
|
+ /**
|
|
+ * The same value as {@link #lock}, or null if it is not an instance of {@link ReentrantLock}.
|
|
+ */
|
|
+ private final @Nullable ReentrantLock innerReentrantLock;
|
|
+
|
|
+ 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.
|
|
+ * <br>
|
|
+ * If the current threads is an {@link AbstractYieldingThread},
|
|
+ * this will yield to other tasks while the lock can not be acquired.
|
|
+ * Otherwise, this will block until the lock is acquired.
|
|
+ */
|
|
+ @PotentiallyYielding
|
|
+ @Override
|
|
+ public void lock() {
|
|
+ // Find out our current yielding thread, if any
|
|
+ @Nullable AbstractYieldingThread yieldingThread = AbstractYieldingThread.currentYieldingThread();
|
|
+ // Try to acquire the lock straight away
|
|
+ if (!this.innerLock.tryLock()) {
|
|
+ // If we are not on a yielding thread, we wait for the lock instead of yielding
|
|
+ if (yieldingThread == null) {
|
|
+ this.innerLock.lock();
|
|
+ return;
|
|
+ }
|
|
+ // Otherwise, we yield to other tasks until the lock can be acquired
|
|
+ yieldingThread.yieldUntil(null, this);
|
|
+ }
|
|
+ // Increment the YieldingLock count of the current thread
|
|
+ if (yieldingThread != null) {
|
|
+ yieldingThread.incrementHeldYieldingLockCount();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Releases the lock (must be called after having completed the computation block that required the lock).
|
|
+ */
|
|
+ @Override
|
|
+ public void unlock() {
|
|
+ this.innerLock.unlock();
|
|
+ // Decrement the YieldingLock count of the current thread
|
|
+ @Nullable AbstractYieldingThread yieldingThread = AbstractYieldingThread.currentYieldingThread();
|
|
+ if (yieldingThread != null) {
|
|
+ yieldingThread.decrementHeldYieldingLockCount();
|
|
+ }
|
|
+ // Potentially signal a thread that this lock has become available.
|
|
+ // Another thread could also acquire the lock at this moment, so when we signal the thread we obtain below,
|
|
+ // it may already be too late for the polled thread to acquire this lock
|
|
+ // (but note that the same thread cannot have been added again because only the thread itself can do that -
|
|
+ // and it is still waiting).
|
|
+ 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.
|
|
+ * <br>
|
|
+ * 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.
|
|
+ * <br>
|
|
+ * This must not be called on queues in the {@link BaseTaskQueueTier#SERVER} tier by threads that are not an
|
|
+ * instance of {@link ServerThread}.
|
|
+ * <br>
|
|
+ * This must not be called on queues apart from the {@link BaseTaskQueueTier#SERVER} tier by threads that are an
|
|
+ * instance of {@link ServerThread}. Use {@link #pollTiny(BaseThread)} instead.
|
|
+ *
|
|
+ * @param currentThread The current thread.
|
|
+ * @return The polled task, or null if this queue was empty.
|
|
+ */
|
|
+ @BaseThreadOnly
|
|
+ @Nullable Runnable poll(BaseThread currentThread);
|
|
+
|
|
+ /**
|
|
+ * Attempts to poll a {@link TaskSpan#TINY} task.
|
|
+ *
|
|
+ * @see #poll(BaseThread)
|
|
+ */
|
|
+ @BaseThreadOnly
|
|
+ @Nullable Runnable pollTiny(BaseThread currentThread);
|
|
+
|
|
+ /**
|
|
+ * Schedules a new task to this queue.
|
|
+ *
|
|
+ * @param task The task to schedule.
|
|
+ * @param span The {@link TaskSpan} of the task.
|
|
+ */
|
|
+ void add(Runnable task, TaskSpan span);
|
|
+
|
|
+ /**
|
|
+ * Sets the tier of this queue. This ensures queues do not accidentally initialize the tiers
|
|
+ * before the tiers have finished initializing themselves.
|
|
+ */
|
|
+ void setTier(BaseTaskQueueTier tier);
|
|
+
|
|
+ /**
|
|
+ * @return Whether any of the given task queues is non-empty.
|
|
+ */
|
|
+ static boolean taskQueuesHaveTasks(AbstractTaskQueue[] queues) {
|
|
+ for (AbstractTaskQueue queue : queues) {
|
|
+ if (queue.hasTasks()) {
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+}
|
|
diff --git a/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledTaskQueue.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..bbe92d92f58d484084114da73003c345d00aac7e
|
|
--- /dev/null
|
|
+++ b/src/main/java/org/galemc/gale/executor/queue/AllLevelsScheduledTaskQueue.java
|
|
@@ -0,0 +1,127 @@
|
|
+// Gale - base thread pool
|
|
+
|
|
+package org.galemc.gale.executor.queue;
|
|
+
|
|
+import net.minecraft.server.MinecraftServer;
|
|
+import net.minecraft.server.level.ServerLevel;
|
|
+import org.galemc.gale.executor.TaskSpan;
|
|
+import org.galemc.gale.executor.annotation.thread.AnyThreadSafe;
|
|
+import org.galemc.gale.executor.annotation.YieldFree;
|
|
+import org.galemc.gale.executor.thread.BaseThread;
|
|
+import org.galemc.gale.executor.thread.pool.BaseThreadActivation;
|
|
+import org.jetbrains.annotations.Nullable;
|
|
+
|
|
+/**
|
|
+ * Common implementation for queues with scheduled tasks for all levels.
|
|
+ * <br>
|
|
+ * All tasks provided by this queue must be yield-free.
|
|
+ *
|
|
+ * @author Martijn Muijsers under AGPL-3.0
|
|
+ */
|
|
+@AnyThreadSafe
|
|
+@YieldFree
|
|
+public abstract class AllLevelsScheduledTaskQueue implements AbstractTaskQueue {
|
|
+
|
|
+ /**
|
|
+ * 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/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..cc5373e78657d04a43cb844c4fcd5f0f9cacf187
|
|
--- /dev/null
|
|
+++ b/src/main/java/org/galemc/gale/executor/queue/BaseTaskQueueTier.java
|
|
@@ -0,0 +1,115 @@
|
|
+// 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.
|
|
+ * <br>
|
|
+ * 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}.
|
|
+ * <br>
|
|
+ * Some parts of the server can only be safely accessed by one thread at a time.
|
|
+ * If they can not be guarded by a lock (or if this is not desired,
|
|
+ * because if a ticking thread would need to acquire this lock it would block it),
|
|
+ * then these parts of the code are typically deferred to the server thread.
|
|
+ * Based on the current use of the {@link TickThread} class, particularly given the existence of
|
|
+ * {@link TickThread#isTickThreadFor(Entity)} and {@link TickThread#isTickThreadFor(ServerLevel, int, int)},
|
|
+ * we can deduce that future support for performing some of these actions in parallel is planned.
|
|
+ * In such a case, some server thread tasks may become tasks that must be
|
|
+ * executed on an appropriate {@link TickThread}.
|
|
+ * In that case, the queues below should be changed so that the server thread and any of the
|
|
+ * ticking threads poll from queues that contain tasks appropriate for them.
|
|
+ * For example, {@link BaseTaskQueues#deferredToUniversalTickThread} would be for tasks that can run
|
|
+ * on any ticking thread, and additional queues would need to be added concerning a specific
|
|
+ * subject (like an entity or chunk) with tasks that will be run on whichever ticking thread is the
|
|
+ * ticking thread for that subject at the time of polling.
|
|
+ */
|
|
+ SERVER(new AbstractTaskQueue[]{
|
|
+ BaseTaskQueues.deferredToServerThread,
|
|
+ BaseTaskQueues.serverThreadTick,
|
|
+ BaseTaskQueues.anyTickScheduledServerThread
|
|
+ }, MinecraftServer.SERVER_THREAD_PRIORITY),
|
|
+ /**
|
|
+ * A tier for queues that contain tasks that are part of ticking,
|
|
+ * to assist the main ticking thread(s) in doing so.
|
|
+ */
|
|
+ TICK_ASSIST(new AbstractTaskQueue[]{
|
|
+ BaseTaskQueues.tickAssist
|
|
+ }, Integer.getInteger("gale.thread.priority.tick", 7)),
|
|
+ /**
|
|
+ * A tier for queues that contain general tasks that must be performed at some point in time,
|
|
+ * asynchronously with respect to the {@link ServerThread} and the ticking of the server.
|
|
+ * Execution of
|
|
+ */
|
|
+ ASYNC(new AbstractTaskQueue[0], Integer.getInteger("gale.thread.priority.async", 6)),
|
|
+ /**
|
|
+ * A tier for queues that contain tasks with the same considerations as {@link #ASYNC},
|
|
+ * but with a low priority.
|
|
+ */
|
|
+ LOW_PRIORITY_ASYNC(new AbstractTaskQueue[0], Integer.getInteger("gale.thread.priority.async.low", 3));
|
|
+
|
|
+ /**
|
|
+ * Equal to {@link #ordinal()}.
|
|
+ */
|
|
+ public final int ordinal;
|
|
+
|
|
+ /**
|
|
+ * The task queues that belong to this tier.
|
|
+ */
|
|
+ public final AbstractTaskQueue[] taskQueues;
|
|
+
|
|
+ /**
|
|
+ * The priority for threads that are executing a task from this tier.
|
|
+ * <br>
|
|
+ * If a thread yields to other tasks, the priority it will have is always the highest priority of any task
|
|
+ * on its stack.
|
|
+ */
|
|
+ public final int threadPriority;
|
|
+
|
|
+ BaseTaskQueueTier(AbstractTaskQueue[] taskQueues, int threadPriority) {
|
|
+ this.ordinal = this.ordinal();
|
|
+ this.taskQueues = taskQueues;
|
|
+ for (AbstractTaskQueue queue : this.taskQueues) {
|
|
+ queue.setTier(this);
|
|
+ }
|
|
+ this.threadPriority = threadPriority;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Equal to {@link #values()}.
|
|
+ */
|
|
+ public static final BaseTaskQueueTier[] VALUES = values();
|
|
+
|
|
+ /**
|
|
+ * Equal to {@link #VALUES}{@code .length}.
|
|
+ */
|
|
+ public static final int length = VALUES.length;
|
|
+
|
|
+ /**
|
|
+ * Equal to {@link #VALUES} without {@link #SERVER}.
|
|
+ */
|
|
+ public static final BaseTaskQueueTier[] VALUES_EXCEPT_SERVER = Arrays.stream(VALUES).filter(tier -> tier != SERVER).toList().toArray(new BaseTaskQueueTier[length - 1]);
|
|
+
|
|
+}
|
|
diff --git a/src/main/java/org/galemc/gale/executor/queue/BaseTaskQueues.java b/src/main/java/org/galemc/gale/executor/queue/BaseTaskQueues.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..cf8e2b42ecfc8205af5b105e19975c3e54ffec5f
|
|
--- /dev/null
|
|
+++ b/src/main/java/org/galemc/gale/executor/queue/BaseTaskQueues.java
|
|
@@ -0,0 +1,92 @@
|
|
+// Gale - base thread pool
|
|
+
|
|
+package org.galemc.gale.executor.queue;
|
|
+
|
|
+import io.papermc.paper.util.MCUtil;
|
|
+import io.papermc.paper.util.TickThread;
|
|
+import org.galemc.gale.executor.TaskSpan;
|
|
+import org.galemc.gale.executor.thread.BaseThread;
|
|
+import org.galemc.gale.executor.thread.deferral.TickThreadDeferral;
|
|
+import org.galemc.gale.executor.thread.AbstractYieldingThread;
|
|
+
|
|
+/**
|
|
+ * This class statically provides a list of task queues containing tasks for {@link AbstractYieldingThread}s to poll from.
|
|
+ *
|
|
+ * @author Martijn Muijsers under AGPL-3.0
|
|
+ */
|
|
+public final class BaseTaskQueues {
|
|
+
|
|
+ private BaseTaskQueues() {}
|
|
+
|
|
+ /**
|
|
+ * This queue stores the tasks scheduled to be executed on a {@link TickThread}, that are procedures that must run
|
|
+ * on a tick thread, but their completion is currently needed by another task that has started running on a thread
|
|
+ * that was not the main thread at the time of scheduling the main-thread-only procedure.
|
|
+ * <br>
|
|
+ * These tasks are explicitly those that other tasks are waiting on, and as such always have a higher priority
|
|
+ * in being started than pending tasks in represent steps in ticking the server, and as such always have the
|
|
+ * higher priority in being started than pending tasks in {@link #serverThreadTick}.
|
|
+ * <br>
|
|
+ * This queue may contain tasks of every {@link TaskSpan}.
|
|
+ * <br>
|
|
+ * This queue's {@link AbstractTaskQueue#add} must not be called from the server thread,
|
|
+ * because the server thread must not defer to itself (because tasks in this queue are assumed to have to run
|
|
+ * independent of other server thread tasks, therefore this will cause a deadlock due to the scheduled task
|
|
+ * never starting).
|
|
+ * Instead, any task that must be deferred to the main thread must instead simply be executed
|
|
+ * when encountered on the main thread.
|
|
+ */
|
|
+ public static final 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.
|
|
+ * <br>
|
|
+ * This queue may contain tasks of every {@link TaskSpan}.
|
|
+ * <br>
|
|
+ * This is currently completely unused, because {@link TickThreadDeferral} simply adds task to
|
|
+ * {@link #deferredToServerThread} instead, since there are currently no special {@link TickThread}s.
|
|
+ */
|
|
+ @SuppressWarnings("unused")
|
|
+ public static final 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}.
|
|
+ * <br>
|
|
+ * This queue may contain tasks of every {@link TaskSpan}.
|
|
+ * <br>
|
|
+ * Tasks in every queue are performed in the order they are added (first-in, first-out). Note that this means
|
|
+ * not all main-thread-only tick tasks are necessarily performed in the order they are added, because they may be
|
|
+ * in different queues: either the queue for potentially yielding tasks or the queue for yield-free tasks.
|
|
+ */
|
|
+ public static final 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}).
|
|
+ * <br>
|
|
+ * This queue may contain tasks of every {@link TaskSpan}.
|
|
+ * <br>
|
|
+ * Tasks in every queue are performed in the order they are added (first-in, first-out). Note that this means
|
|
+ * not all {@link BaseThread} tick tasks are necessarily performed in the order they are added, because they may be
|
|
+ * in different queues: either the queue for potentially yielding tasks or the queue for yield-free tasks.
|
|
+ */
|
|
+ public static final SimpleTaskQueue tickAssist = SimpleTaskQueue.allSpans("TickAssist");
|
|
+
|
|
+}
|
|
diff --git a/src/main/java/org/galemc/gale/executor/queue/ScheduledServerThreadTaskQueues.java b/src/main/java/org/galemc/gale/executor/queue/ScheduledServerThreadTaskQueues.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..a763028deff9bc131b8208886bf2651373b1dbc3
|
|
--- /dev/null
|
|
+++ b/src/main/java/org/galemc/gale/executor/queue/ScheduledServerThreadTaskQueues.java
|
|
@@ -0,0 +1,287 @@
|
|
+// Gale - base thread pool
|
|
+
|
|
+package org.galemc.gale.executor.queue;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
|
|
+import net.minecraft.server.MinecraftServer;
|
|
+import net.minecraft.util.thread.BlockableEventLoop;
|
|
+import org.galemc.gale.concurrent.UnterminableExecutorService;
|
|
+import org.galemc.gale.executor.annotation.Access;
|
|
+import org.galemc.gale.executor.annotation.thread.AnyThreadSafe;
|
|
+import org.galemc.gale.executor.annotation.Guarded;
|
|
+import org.galemc.gale.executor.annotation.YieldFree;
|
|
+import org.galemc.gale.executor.thread.pool.BaseThreadActivation;
|
|
+import org.galemc.gale.executor.thread.ServerThread;
|
|
+import org.galemc.gale.executor.TaskSpan;
|
|
+import org.jetbrains.annotations.NotNull;
|
|
+import org.jetbrains.annotations.Nullable;
|
|
+
|
|
+import java.util.Queue;
|
|
+import java.util.concurrent.Executor;
|
|
+import java.util.concurrent.ExecutorService;
|
|
+import java.util.concurrent.locks.Lock;
|
|
+import java.util.concurrent.locks.ReadWriteLock;
|
|
+import java.util.concurrent.locks.ReentrantReadWriteLock;
|
|
+
|
|
+/**
|
|
+ * This class stores the tasks scheduled to be executed on the main thread, which would usually be stored in a queue
|
|
+ * in the {@link MinecraftServer} in its role as a {@link BlockableEventLoop}. These tasks are not steps of
|
|
+ * ticking, but other tasks that must be executed at some point while no other main-thread-only task is running.
|
|
+ * <br>
|
|
+ * Each task is stored by the number of ticks left before it has to be executed. Tasks will always definitely be
|
|
+ * executed within the required number of ticks. Tasks may also be executed earlier than needed
|
|
+ * if there is time to do so.
|
|
+ * <br>
|
|
+ * Note that this means not all tasks are necessarily performed in the order they are added, because they may be in
|
|
+ * different queues based on the number of ticks before they have to be executed.
|
|
+ * <br>
|
|
+ * All contained tasks are currently assumed to be {@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<>() };
|
|
+
|
|
+ /**
|
|
+ * <i>Probably</i> the lowest index of any queue in {@link #queues} that is non-empty.
|
|
+ * This is maintained as well as possible.
|
|
+ */
|
|
+ public static volatile int firstQueueWithPotentialTasksIndex = 0;
|
|
+
|
|
+ @Guarded(value = "#lock", fieldAccess = Access.WRITE)
|
|
+ public static volatile long versionStamp = 0;
|
|
+
|
|
+ private static final ReadWriteLock lock = new ReentrantReadWriteLock();
|
|
+ private static final Lock readLock = lock.readLock();
|
|
+ public static final Lock writeLock = lock.writeLock();
|
|
+
|
|
+ /**
|
|
+ * @return Whether there are any scheduled main thread tasks, not counting any tasks that do not have to be
|
|
+ * finished within the current tick if {@code tryNonCurrentTickQueuesAtAll} is false.
|
|
+ */
|
|
+ public static boolean hasTasks(boolean tryNonCurrentTickQueuesAtAll) {
|
|
+ while (!readLock.tryLock()) {
|
|
+ Thread.onSpinWait();
|
|
+ }
|
|
+ try {
|
|
+ // Try the queue most likely to contain tasks first
|
|
+ if (firstQueueWithPotentialTasksIndex == 0 || tryNonCurrentTickQueuesAtAll) {
|
|
+ if (!queues[firstQueueWithPotentialTasksIndex].isEmpty()) {
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+ int checkUpTo = tryNonCurrentTickQueuesAtAll ? queues.length : 1;
|
|
+ for (int i = 0; i < checkUpTo; i++) {
|
|
+ if (!queues[i].isEmpty()) {
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+ } finally {
|
|
+ readLock.unlock();
|
|
+ }
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Polls a task from this queue and returns it.
|
|
+ * Tasks that are scheduled for a later tick will only be regarded as able to be started if both we are not out of
|
|
+ * spare time this tick and {@code tryNonCurrentTickQueuesAtAll} is true.
|
|
+ * <br>
|
|
+ * This method does not check whether the given thread is or could claim the main thread: whether a task
|
|
+ * can start now is thread-agnostic and based purely on the state of the queue.
|
|
+ *
|
|
+ * @return The task that was polled, or null if no task was found.
|
|
+ */
|
|
+ public static @Nullable Runnable poll(ServerThread currentThread, boolean tryNonCurrentTickQueuesAtAll) {
|
|
+ // Since we assume the tasks in this queue to be potentially yielding, fail if the thread is restricted
|
|
+ if (!currentThread.canStartYieldingTasks) {
|
|
+ return null;
|
|
+ }
|
|
+ pollFromFirstQueueOrOthers: while (true) {
|
|
+ // Try to get a task from the first queue first
|
|
+ Object task = queues[0].poll();
|
|
+ if (task != null) {
|
|
+ return (Runnable) task;
|
|
+ } else if (!tryNonCurrentTickQueuesAtAll || MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking) {
|
|
+ // We needed a task from the first queue
|
|
+ if (queues[0].isEmpty()) {
|
|
+ // The first queue really is empty, so we fail
|
|
+ return null;
|
|
+ }
|
|
+ // An element was added to the first queue in the meantime, try again
|
|
+ continue;
|
|
+ }
|
|
+ tryGetRunnableUntilSuccessOrNothingChanged: while (true) {
|
|
+ boolean goOverAllQueues = firstQueueWithPotentialTasksIndex == 0;
|
|
+ long oldVersionStamp = versionStamp;
|
|
+ for (int i = goOverAllQueues ? 0 : firstQueueWithPotentialTasksIndex; i < queues.length; i++) {
|
|
+ if (!queues[i].isEmpty()) {
|
|
+ if (i == 0 || !MinecraftServer.isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking) {
|
|
+ task = queues[i].poll();
|
|
+ if (task == null) {
|
|
+ // We lost a race condition between the isEmpty() and poll() calls: just try again
|
|
+ continue tryGetRunnableUntilSuccessOrNothingChanged;
|
|
+ }
|
|
+ return (Runnable) task;
|
|
+ }
|
|
+ // Apparently we must now poll from the first queue only, try again
|
|
+ continue pollFromFirstQueueOrOthers;
|
|
+ }
|
|
+ // The queue was empty, the first queue with potential tasks must be the next one
|
|
+ if (i == firstQueueWithPotentialTasksIndex) {
|
|
+ if (i == queues.length - 1) {
|
|
+ firstQueueWithPotentialTasksIndex = 0;
|
|
+ } else {
|
|
+ firstQueueWithPotentialTasksIndex = i + 1;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ if (goOverAllQueues && firstQueueWithPotentialTasksIndex == 0 && oldVersionStamp == versionStamp) {
|
|
+ /*
|
|
+ We went over all queues and nothing changed in the meantime,
|
|
+ we give up, there appear to be no more tasks.
|
|
+ */
|
|
+ return null;
|
|
+ }
|
|
+ // Something changed, or we did not go over all queues, try again
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static int getTaskCount() {
|
|
+ while (!readLock.tryLock()) {
|
|
+ Thread.onSpinWait();
|
|
+ }
|
|
+ try {
|
|
+ int count = 0;
|
|
+ for (Queue<?> queue : queues) {
|
|
+ count += queue.size();
|
|
+ }
|
|
+ return count;
|
|
+ } finally {
|
|
+ readLock.unlock();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Schedules a new task to this queue.
|
|
+ *
|
|
+ * @param task The task to schedule.
|
|
+ *
|
|
+ * @deprecated Use {@link #add(Runnable, int)} instead: do not rely on {@link #DEFAULT_TASK_MAX_DELAY}.
|
|
+ */
|
|
+ @Deprecated
|
|
+ public static void add(Runnable task) {
|
|
+ add(task, DEFAULT_TASK_MAX_DELAY);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Schedules a new task to this queue.
|
|
+ *
|
|
+ * @param task The task to schedule.
|
|
+ * @param maxDelay The maximum number of ticks that the task must be finished by.
|
|
+ * A value of 0 means the task must be finished before the end of the current tick.
|
|
+ */
|
|
+ public static void add(Runnable task, int maxDelay) {
|
|
+ // Paper start - anything that does try to post to main during watchdog crash, run on watchdog
|
|
+ if (MinecraftServer.SERVER.hasStopped && Thread.currentThread() == MinecraftServer.SERVER.shutdownThread) {
|
|
+ task.run();
|
|
+ return;
|
|
+ }
|
|
+ // Paper end - anything that does try to post to main during watchdog crash, run on watchdog
|
|
+ while (!readLock.tryLock()) {
|
|
+ Thread.onSpinWait();
|
|
+ }
|
|
+ try {
|
|
+ //noinspection NonAtomicOperationOnVolatileField
|
|
+ versionStamp++;
|
|
+ //noinspection unchecked
|
|
+ queues[maxDelay].add(task);
|
|
+ if (maxDelay < firstQueueWithPotentialTasksIndex) {
|
|
+ firstQueueWithPotentialTasksIndex = maxDelay;
|
|
+ }
|
|
+ //noinspection NonAtomicOperationOnVolatileField
|
|
+ versionStamp++;
|
|
+ } finally {
|
|
+ readLock.unlock();
|
|
+ }
|
|
+ MinecraftServer.nextTimeAssumeWeMayHaveDelayedTasks = true;
|
|
+ BaseThreadActivation.newTaskWasAdded(tier, TaskSpan.YIELDING);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * This moves the queues in {@link #queues}.
|
|
+ */
|
|
+ public static void shiftTasksForNextTick() {
|
|
+ while (!writeLock.tryLock()) {
|
|
+ Thread.onSpinWait();
|
|
+ }
|
|
+ try {
|
|
+ //noinspection NonAtomicOperationOnVolatileField
|
|
+ versionStamp++;
|
|
+ // Move the queues to the preceding position
|
|
+ Queue<?> firstQueue = queues[0];
|
|
+ for (int i = 1; i < queues.length; i++) {
|
|
+ queues[i - 1] = queues[i];
|
|
+ }
|
|
+ // Re-use the same instance that was the old first queue as the new last queue
|
|
+ queues[queues.length - 1] = firstQueue;
|
|
+ // Move any elements that were still present in the previous first queue to the new first queue
|
|
+ //noinspection unchecked
|
|
+ queues[0].addAll(firstQueue);
|
|
+ firstQueue.clear();
|
|
+ //noinspection NonAtomicOperationOnVolatileField
|
|
+ versionStamp++;
|
|
+ } finally {
|
|
+ writeLock.unlock();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * An executor for adding tasks to this queue,
|
|
+ * where {@link Executor#execute} calls {@link #add}.
|
|
+ *
|
|
+ * @deprecated Use {@link #add(Runnable, int)} instead: do not rely on {@link #DEFAULT_TASK_MAX_DELAY}.
|
|
+ */
|
|
+ @Deprecated
|
|
+ public static final ExecutorService executor = new UnterminableExecutorService() {
|
|
+
|
|
+ @Override
|
|
+ public void execute(@NotNull Runnable runnable) {
|
|
+ add(runnable);
|
|
+ }
|
|
+
|
|
+ };
|
|
+
|
|
+}
|
|
diff --git a/src/main/java/org/galemc/gale/executor/queue/SimpleTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/SimpleTaskQueue.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..2c910f89f1056d00e5e4a2d832cdd4be4b7527b4
|
|
--- /dev/null
|
|
+++ b/src/main/java/org/galemc/gale/executor/queue/SimpleTaskQueue.java
|
|
@@ -0,0 +1,253 @@
|
|
+// Gale - base thread pool
|
|
+
|
|
+package org.galemc.gale.executor.queue;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
|
|
+import org.galemc.gale.collection.FIFOConcurrentLinkedQueue;
|
|
+import org.galemc.gale.concurrent.UnterminableExecutorService;
|
|
+import org.galemc.gale.executor.TaskSpan;
|
|
+import org.galemc.gale.executor.annotation.thread.AnyThreadSafe;
|
|
+import org.galemc.gale.executor.annotation.YieldFree;
|
|
+import org.galemc.gale.executor.thread.BaseThread;
|
|
+import org.galemc.gale.executor.thread.pool.BaseThreadActivation;
|
|
+import org.galemc.gale.executor.thread.SignalReason;
|
|
+import org.jetbrains.annotations.NotNull;
|
|
+import org.jetbrains.annotations.Nullable;
|
|
+
|
|
+import java.util.Arrays;
|
|
+import java.util.Queue;
|
|
+import java.util.concurrent.Executor;
|
|
+import java.util.concurrent.ExecutorService;
|
|
+
|
|
+/**
|
|
+ * A base class for a task queue that may contain tasks that are
|
|
+ * potentially yielding and tasks that are yield-free.
|
|
+ *
|
|
+ * @author Martijn Muijsers under AGPL-3.0
|
|
+ */
|
|
+@AnyThreadSafe
|
|
+@YieldFree
|
|
+public class SimpleTaskQueue implements AbstractTaskQueue {
|
|
+
|
|
+ /**
|
|
+ * The name of this queue.
|
|
+ *
|
|
+ * @see #getName()
|
|
+ */
|
|
+ public final String name;
|
|
+
|
|
+ /**
|
|
+ * The {@link BaseTaskQueueTier} that contains this queue.
|
|
+ * Will be initialized in {@link #setTier} by the constructor of the tier.
|
|
+ */
|
|
+ public BaseTaskQueueTier tier;
|
|
+
|
|
+ /**
|
|
+ * Whether tasks in this queue can have a {@link TaskSpan}, indexed by the span's {@link TaskSpan#ordinal}.
|
|
+ */
|
|
+ public final boolean[] canHaveTasks;
|
|
+
|
|
+ /**
|
|
+ * The queues of tasks, indexed by their {@link TaskSpan#ordinal}. Individual elements may be null if the
|
|
+ * corresponding {@link #canHaveTasks} value is false.
|
|
+ */
|
|
+ @SuppressWarnings("rawtypes")
|
|
+ private final @Nullable Queue @NotNull [] queues;
|
|
+
|
|
+ /**
|
|
+ * An executor for adding tasks of a specific {@link TaskSpan} to this queue, where {@link Executor#execute}
|
|
+ * calls {@link #add(Runnable, TaskSpan)},
|
|
+ * or null if the corresponding value in {@link #canHaveTasks} is false.
|
|
+ */
|
|
+ @SuppressWarnings("FieldCanBeLocal")
|
|
+ private final @Nullable ExecutorService @NotNull [] executors;
|
|
+
|
|
+ /**
|
|
+ * An executor for adding {@link TaskSpan#YIELDING} tasks to this queue,
|
|
+ * where {@link Executor#execute} calls {@link #add},
|
|
+ * or null if the corresponding value in {@link #canHaveTasks} is false.
|
|
+ */
|
|
+ public final ExecutorService yieldingExecutor;
|
|
+
|
|
+ /**
|
|
+ * An executor for adding {@link TaskSpan#FREE} tasks to this queue,
|
|
+ * where {@link Executor#execute} calls {@link #add},
|
|
+ * or null if the corresponding value in {@link #canHaveTasks} is false.
|
|
+ */
|
|
+ public final ExecutorService freeExecutor;
|
|
+
|
|
+ /**
|
|
+ * An executor for adding {@link TaskSpan#TINY} tasks to this queue,
|
|
+ * where {@link Executor#execute} calls {@link #add},
|
|
+ * or null if the corresponding value in {@link #canHaveTasks} is false.
|
|
+ */
|
|
+ public final ExecutorService tinyExecutor;
|
|
+
|
|
+ SimpleTaskQueue(String name, boolean[] canHaveTasks) {
|
|
+ this(name, canHaveTasks, false);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * @param name Value for {@link #getName}.
|
|
+ * @param canHaveTasks Value for {@link #canHaveTasks}.
|
|
+ * @param lifoQueues If true, the queues in {@link #queues} will be LIFO;
|
|
+ * otherwise, they will be FIFO.
|
|
+ */
|
|
+ SimpleTaskQueue(String name, boolean[] canHaveTasks, boolean lifoQueues) {
|
|
+ this.name = name;
|
|
+ if (canHaveTasks.length != TaskSpan.length) {
|
|
+ throw new IllegalArgumentException();
|
|
+ }
|
|
+ this.canHaveTasks = canHaveTasks;
|
|
+ this.queues = new Queue[TaskSpan.length];
|
|
+ this.executors = new ExecutorService[TaskSpan.length];
|
|
+ for (int spanOrdinal = 0; spanOrdinal < TaskSpan.length; spanOrdinal++) {
|
|
+ if (this.canHaveTasks[spanOrdinal]) {
|
|
+ this.queues[spanOrdinal] = lifoQueues ? new FIFOConcurrentLinkedQueue<>() : new MultiThreadedQueue<>();
|
|
+ this.executors[spanOrdinal] = new SpanExecutor(TaskSpan.VALUES[spanOrdinal]);
|
|
+ }
|
|
+ }
|
|
+ this.yieldingExecutor = this.executors[TaskSpan.YIELDING.ordinal];
|
|
+ this.freeExecutor = this.executors[TaskSpan.FREE.ordinal];
|
|
+ this.tinyExecutor = this.executors[TaskSpan.TINY.ordinal];
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public final String getName() {
|
|
+ return this.name;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public final boolean hasTasks() {
|
|
+ for (int spanOrdinal = 0; spanOrdinal < TaskSpan.length; spanOrdinal++) {
|
|
+ var queue = this.queues[spanOrdinal];
|
|
+ if (queue != null && !queue.isEmpty()) {
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean hasTasks(TaskSpan span) {
|
|
+ var queue = this.queues[span.ordinal];
|
|
+ return queue != null && !queue.isEmpty();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean canHaveTasks(TaskSpan span) {
|
|
+ return this.queues[span.ordinal] != null;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * @see #poll(BaseThread)
|
|
+ */
|
|
+ private @Nullable Runnable poll(BaseThread currentThread, int spanOrdinal) {
|
|
+ var queue = this.queues[spanOrdinal];
|
|
+ if (queue != null) {
|
|
+ if (currentThread.canStartYieldingTasks || TaskSpan.VALUES[spanOrdinal].isNotYielding) {
|
|
+ Object task = queue.poll();
|
|
+ if (task != null) {
|
|
+ /*
|
|
+ If the thread was woken up for a different reason,
|
|
+ another thread should be signalled for that reason.
|
|
+ */
|
|
+ SignalReason lastSignalReason = currentThread.lastSignalReason;
|
|
+ if (lastSignalReason != null && lastSignalReason != SignalReason.TASK) {
|
|
+ BaseThreadActivation.callForUpdate();
|
|
+ }
|
|
+ return (Runnable) task;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public final @Nullable Runnable poll(BaseThread currentThread) {
|
|
+ Runnable task;
|
|
+ for (int spanOrdinal = 0; spanOrdinal < TaskSpan.length; spanOrdinal++) {
|
|
+ task = this.poll(currentThread, spanOrdinal);
|
|
+ if (task != null) {
|
|
+ return task;
|
|
+ }
|
|
+ }
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public final @Nullable Runnable pollTiny(BaseThread currentThread) {
|
|
+ return poll(currentThread, TaskSpan.TINY.ordinal);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public final void add(Runnable task, TaskSpan span) {
|
|
+ int spanOrdinal = span.ordinal;
|
|
+ //noinspection unchecked
|
|
+ this.queues[spanOrdinal].add(task);
|
|
+ BaseThreadActivation.newTaskWasAdded(this.tier, span);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public final void setTier(BaseTaskQueueTier tier) {
|
|
+ if (this.tier != null) {
|
|
+ throw new IllegalStateException("SimpleTaskQueue.tier was already initialized");
|
|
+ }
|
|
+ this.tier = tier;
|
|
+ }
|
|
+
|
|
+ private class SpanExecutor extends UnterminableExecutorService {
|
|
+
|
|
+ private final TaskSpan span;
|
|
+
|
|
+ private SpanExecutor(TaskSpan span) {
|
|
+ this.span = span;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void execute(@NotNull Runnable runnable) {
|
|
+ SimpleTaskQueue.this.add(runnable, this.span);
|
|
+ }
|
|
+
|
|
+ }
|
|
+
|
|
+ public static SingleSpanSimpleTaskQueue singleSpan(String name, TaskSpan span) {
|
|
+ return new SingleSpanSimpleTaskQueue(name, span);
|
|
+ }
|
|
+
|
|
+ @SuppressWarnings("unused")
|
|
+ public static SingleSpanSimpleTaskQueue singleSpan(String name, TaskSpan span, boolean lifoQueues) {
|
|
+ return new SingleSpanSimpleTaskQueue(name, span, lifoQueues);
|
|
+ }
|
|
+
|
|
+ private static boolean[] createCanHaveTasksForDoubleSpan(TaskSpan span1, TaskSpan span2) {
|
|
+ boolean[] canHaveTasks = new boolean[TaskSpan.length];
|
|
+ canHaveTasks[span1.ordinal] = true;
|
|
+ canHaveTasks[span2.ordinal] = true;
|
|
+ return canHaveTasks;
|
|
+ }
|
|
+
|
|
+ @SuppressWarnings("unused")
|
|
+ public static SimpleTaskQueue doubleSpan(String name, TaskSpan span1, TaskSpan span2) {
|
|
+ return new SimpleTaskQueue(name, createCanHaveTasksForDoubleSpan(span1, span2));
|
|
+ }
|
|
+
|
|
+ @SuppressWarnings("unused")
|
|
+ public static SimpleTaskQueue doubleSpan(String name, TaskSpan span1, TaskSpan span2, boolean lifoQueues) {
|
|
+ return new SimpleTaskQueue(name, createCanHaveTasksForDoubleSpan(span1, span2), lifoQueues);
|
|
+ }
|
|
+
|
|
+ private static final boolean[] canHaveTasksForAllSpans = new boolean[TaskSpan.length];
|
|
+ static {
|
|
+ Arrays.fill(canHaveTasksForAllSpans, true);
|
|
+ }
|
|
+
|
|
+ public static SimpleTaskQueue allSpans(String name) {
|
|
+ return new SimpleTaskQueue(name, canHaveTasksForAllSpans);
|
|
+ }
|
|
+
|
|
+ public static SimpleTaskQueue allSpans(String name, boolean lifoQueues) {
|
|
+ return new SimpleTaskQueue(name, canHaveTasksForAllSpans, lifoQueues);
|
|
+ }
|
|
+
|
|
+}
|
|
diff --git a/src/main/java/org/galemc/gale/executor/queue/SingleSpanSimpleTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/SingleSpanSimpleTaskQueue.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..2251087670d554a7bd5dc81631615aa0728eb315
|
|
--- /dev/null
|
|
+++ b/src/main/java/org/galemc/gale/executor/queue/SingleSpanSimpleTaskQueue.java
|
|
@@ -0,0 +1,66 @@
|
|
+// Gale - base thread pool
|
|
+
|
|
+package org.galemc.gale.executor.queue;
|
|
+
|
|
+import org.galemc.gale.concurrent.UnterminableExecutorService;
|
|
+import org.galemc.gale.executor.TaskSpan;
|
|
+import org.galemc.gale.executor.annotation.YieldFree;
|
|
+import org.galemc.gale.executor.annotation.thread.AnyThreadSafe;
|
|
+import org.jetbrains.annotations.NotNull;
|
|
+
|
|
+import java.util.concurrent.Executor;
|
|
+import java.util.concurrent.ExecutorService;
|
|
+
|
|
+/**
|
|
+ * A base class for a task queue that contains tasks that are all of one {@link TaskSpan}.
|
|
+ *
|
|
+ * @author Martijn Muijsers under AGPL-3.0
|
|
+ */
|
|
+@YieldFree
|
|
+public class SingleSpanSimpleTaskQueue extends SimpleTaskQueue {
|
|
+
|
|
+ private static final boolean[][] canHaveTasksPerSpan = new boolean[TaskSpan.length][];
|
|
+ static {
|
|
+ for (TaskSpan span : TaskSpan.VALUES) {
|
|
+ canHaveTasksPerSpan[span.ordinal] = new boolean[TaskSpan.length];
|
|
+ canHaveTasksPerSpan[span.ordinal][span.ordinal] = true;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private final TaskSpan span;
|
|
+
|
|
+ SingleSpanSimpleTaskQueue(String name, TaskSpan span) {
|
|
+ super(name, canHaveTasksPerSpan[span.ordinal]);
|
|
+ this.span = span;
|
|
+ }
|
|
+
|
|
+ SingleSpanSimpleTaskQueue(String name, TaskSpan span, boolean lifoQueues) {
|
|
+ super(name, canHaveTasksPerSpan[span.ordinal], lifoQueues);
|
|
+ this.span = span;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Schedules a new task to this queue.
|
|
+ *
|
|
+ * @param task The task to schedule.
|
|
+ */
|
|
+ @AnyThreadSafe
|
|
+ @YieldFree
|
|
+ public void add(Runnable task) {
|
|
+ this.add(task, this.span);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * An executor for adding tasks to this queue,
|
|
+ * where {@link Executor#execute} calls {@link #add}.
|
|
+ */
|
|
+ public final ExecutorService executor = new UnterminableExecutorService() {
|
|
+
|
|
+ @Override
|
|
+ public void execute(@NotNull Runnable runnable) {
|
|
+ SingleSpanSimpleTaskQueue.this.add(runnable, SingleSpanSimpleTaskQueue.this.span);
|
|
+ }
|
|
+
|
|
+ };
|
|
+
|
|
+}
|
|
diff --git a/src/main/java/org/galemc/gale/executor/queue/ThisTickScheduledServerThreadTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/ThisTickScheduledServerThreadTaskQueue.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..3cb2b84cb7653ff3e038acdc2e6e11f805bfbbba
|
|
--- /dev/null
|
|
+++ b/src/main/java/org/galemc/gale/executor/queue/ThisTickScheduledServerThreadTaskQueue.java
|
|
@@ -0,0 +1,27 @@
|
|
+// Gale - base thread pool
|
|
+
|
|
+package org.galemc.gale.executor.queue;
|
|
+
|
|
+import org.galemc.gale.executor.annotation.thread.AnyThreadSafe;
|
|
+import org.galemc.gale.executor.annotation.YieldFree;
|
|
+
|
|
+/**
|
|
+ * This class provides access to, but does not store, the tasks scheduled to be executed on the main thread,
|
|
+ * that must be finished in the current tick or its spare time.
|
|
+ *
|
|
+ * @author Martijn Muijsers under AGPL-3.0
|
|
+ */
|
|
+@AnyThreadSafe
|
|
+@YieldFree
|
|
+public final class ThisTickScheduledServerThreadTaskQueue extends TickRequiredScheduledServerThreadTaskQueue {
|
|
+
|
|
+ ThisTickScheduledServerThreadTaskQueue() {
|
|
+ super(false);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public String getName() {
|
|
+ return "ThisTickScheduledServerThread";
|
|
+ }
|
|
+
|
|
+}
|
|
diff --git a/src/main/java/org/galemc/gale/executor/queue/TickRequiredScheduledServerThreadTaskQueue.java b/src/main/java/org/galemc/gale/executor/queue/TickRequiredScheduledServerThreadTaskQueue.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..fb4f9c047fc71a9a01aa47871254c6a949753a3a
|
|
--- /dev/null
|
|
+++ b/src/main/java/org/galemc/gale/executor/queue/TickRequiredScheduledServerThreadTaskQueue.java
|
|
@@ -0,0 +1,67 @@
|
|
+// Gale - base thread pool
|
|
+
|
|
+package org.galemc.gale.executor.queue;
|
|
+
|
|
+import org.galemc.gale.executor.TaskSpan;
|
|
+import org.galemc.gale.executor.annotation.YieldFree;
|
|
+import org.galemc.gale.executor.annotation.thread.AnyThreadSafe;
|
|
+import org.galemc.gale.executor.thread.BaseThread;
|
|
+import org.galemc.gale.executor.thread.ServerThread;
|
|
+import org.jetbrains.annotations.Nullable;
|
|
+
|
|
+/**
|
|
+ * A common base class for {@link ThisTickScheduledServerThreadTaskQueue} and
|
|
+ * {@link AnyTickScheduledServerThreadTaskQueue}.
|
|
+ * <br>
|
|
+ * 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..d1c305735488ee02e3d86c777ada068da955495b
|
|
--- /dev/null
|
|
+++ b/src/main/java/org/galemc/gale/executor/thread/AbstractYieldingThread.java
|
|
@@ -0,0 +1,68 @@
|
|
+// 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.annotation.thread.ThisThreadOnly;
|
|
+import org.galemc.gale.executor.lock.YieldingLock;
|
|
+import org.jetbrains.annotations.NotNull;
|
|
+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 {
|
|
+
|
|
+ /**
|
|
+ * @return Whether this thread currently holds any {@link YieldingLock}.
|
|
+ */
|
|
+ @ThisThreadOnly
|
|
+ @YieldFree
|
|
+ boolean holdsYieldingLock();
|
|
+
|
|
+ /**
|
|
+ * Increments the number of times this thread is holding a {@link YieldingLock}.
|
|
+ * A thread can hold one {@link YieldingLock} multiple times (it can be reentrant).
|
|
+ */
|
|
+ @ThisThreadOnly
|
|
+ @YieldFree
|
|
+ void incrementHeldYieldingLockCount();
|
|
+
|
|
+ /**
|
|
+ * Decrements the number of times this thread is holding a {@link YieldingLock}.
|
|
+ *
|
|
+ * @see #incrementHeldYieldingLockCount()
|
|
+ */
|
|
+ @ThisThreadOnly
|
|
+ @YieldFree
|
|
+ void decrementHeldYieldingLockCount();
|
|
+
|
|
+ @ThisThreadOnly
|
|
+ void yieldUntil(@Nullable BooleanSupplier stopCondition, @Nullable YieldingLock yieldingLock);
|
|
+
|
|
+ @ThisThreadOnly
|
|
+ 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..633c1aeddca31f5fe95cc8c9ad54e6a2cf1b4ac1
|
|
--- /dev/null
|
|
+++ b/src/main/java/org/galemc/gale/executor/thread/BaseThread.java
|
|
@@ -0,0 +1,721 @@
|
|
+// Gale - base thread pool
|
|
+
|
|
+package org.galemc.gale.executor.thread;
|
|
+
|
|
+import io.papermc.paper.util.TickThread;
|
|
+import net.minecraft.server.MinecraftServer;
|
|
+import org.galemc.gale.executor.TaskSpan;
|
|
+import org.galemc.gale.executor.annotation.Access;
|
|
+import org.galemc.gale.executor.annotation.PotentiallyYielding;
|
|
+import org.galemc.gale.executor.annotation.thread.AnyThreadSafe;
|
|
+import org.galemc.gale.executor.annotation.Guarded;
|
|
+import org.galemc.gale.executor.annotation.PotentiallyBlocking;
|
|
+import org.galemc.gale.executor.annotation.thread.ThisThreadOnly;
|
|
+import org.galemc.gale.executor.annotation.YieldFree;
|
|
+import org.galemc.gale.executor.lock.YieldingLock;
|
|
+import org.galemc.gale.executor.queue.AbstractTaskQueue;
|
|
+import org.galemc.gale.executor.queue.BaseTaskQueueTier;
|
|
+import org.galemc.gale.executor.thread.pool.*;
|
|
+import org.jetbrains.annotations.Nullable;
|
|
+
|
|
+import java.util.concurrent.TimeUnit;
|
|
+import java.util.concurrent.locks.Condition;
|
|
+import java.util.concurrent.locks.Lock;
|
|
+import java.util.concurrent.locks.ReentrantLock;
|
|
+import java.util.function.BooleanSupplier;
|
|
+
|
|
+/**
|
|
+ * An abstract base class implementing {@link AbstractYieldingThread},
|
|
+ * that provides implementation that is common between
|
|
+ * {@link TickThread} and {@link AssistThread}.
|
|
+ *
|
|
+ * @author Martijn Muijsers under AGPL-3.0
|
|
+ */
|
|
+public abstract class BaseThread extends Thread implements AbstractYieldingThread {
|
|
+
|
|
+ /**
|
|
+ * The minimum time to wait as the {@link MinecraftServer#serverThread} when performing a timed wait.
|
|
+ * Given in nanoseconds.
|
|
+ * If a timed wait with a lower time is attempted, the wait is not performed at all.
|
|
+ */
|
|
+ public static final long SERVER_THREAD_WAIT_NANOS_MINIMUM = 10_000;
|
|
+
|
|
+ /**
|
|
+ * The time to wait as the {@link MinecraftServer#serverThread} during the oversleep phase, if
|
|
+ * there may be delayed tasks.
|
|
+ * Given in nanoseconds.
|
|
+ */
|
|
+ public static final long SERVER_THREAD_WAIT_NANOS_DURING_OVERSLEEP_WITH_DELAYED_TASKS = 50_000;
|
|
+
|
|
+ /**
|
|
+ * The index of this thread, as needed as an argument to
|
|
+ * {@link BaseThreadPool#getThreadByBaseIndex(int)}.
|
|
+ */
|
|
+ public final int baseThreadIndex;
|
|
+
|
|
+ /**
|
|
+ * The maximum yield depth for this thread,
|
|
+ * which equals 1 for a {@link ServerThread}
|
|
+ * and {@link AssistThread#MAXIMUM_YIELD_DEPTH} for an {@link AssistThread}.
|
|
+ */
|
|
+ public final int maximumYieldDepth;
|
|
+
|
|
+ /**
|
|
+ * The number of times this thread holds a {@link YieldingLock},
|
|
+ * used in {@link #holdsYieldingLock()}.
|
|
+ *
|
|
+ * @see AbstractYieldingThread#incrementHeldYieldingLockCount()
|
|
+ */
|
|
+ @ThisThreadOnly
|
|
+ public int heldYieldingLockCount = 0;
|
|
+
|
|
+ /**
|
|
+ * The current yield depth of this thread.
|
|
+ */
|
|
+ @AnyThreadSafe(Access.READ) @ThisThreadOnly(Access.WRITE)
|
|
+ public volatile int yieldDepth = 0;
|
|
+
|
|
+ /**
|
|
+ * Whether this thread can currently start yielding tasks with respect to being restricted
|
|
+ * due to {@link #yieldDepth} being at least {@link #maximumYieldDepth}.
|
|
+ * <br>
|
|
+ * This is updated using {@link #updateCanStartYieldingTasks()}
|
|
+ * after {@link #yieldDepth} or {@link #heldYieldingLockCount} is changed.
|
|
+ */
|
|
+ @AnyThreadSafe(Access.READ) @ThisThreadOnly(Access.WRITE)
|
|
+ public volatile boolean canStartYieldingTasks = true;
|
|
+
|
|
+ /**
|
|
+ * The highest {@link BaseTaskQueueTier} of any task on the yielding execution stack of this thread,
|
|
+ * or null if there is no task being executed on this thread.
|
|
+ */
|
|
+ @AnyThreadSafe(Access.READ) @ThisThreadOnly(Access.WRITE)
|
|
+ public volatile @Nullable BaseTaskQueueTier highestTierOfTaskOnStack;
|
|
+
|
|
+ /**
|
|
+ * The {@link BaseTaskQueueTier} that the last non-null return value of {@link #pollTask} was polled from,
|
|
+ * or null if {@link #pollTask} has never been called yet.
|
|
+ */
|
|
+ @ThisThreadOnly
|
|
+ private @Nullable BaseTaskQueueTier lastPolledTaskTier;
|
|
+
|
|
+ /**
|
|
+ * The lock to guard this thread's sleeping and waking actions.
|
|
+ */
|
|
+ private final Lock waitLock = new ReentrantLock();
|
|
+
|
|
+ /**
|
|
+ * The condition to wait for a signal, when this thread has to wait for something to do.
|
|
+ */
|
|
+ private final Condition waitCondition = waitLock.newCondition();
|
|
+
|
|
+ /**
|
|
+ * Whether this thread is currently not working on the content of a task, but instead
|
|
+ * attempting to poll a next task to do, checking whether it can accept tasks at all, or
|
|
+ * attempting to acquire a {@link YieldingLock}, or waiting (although the fact that this value is true during
|
|
+ * waiting is irrelevant, because at such a time, {@link #isWaiting} will be true, and this value will no longer
|
|
+ * have any effect due to the implementation of {@link #signal}).
|
|
+ * <br>
|
|
+ * 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.
|
|
+ * <br>
|
|
+ * 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;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean holdsYieldingLock() {
|
|
+ return this.heldYieldingLockCount > 0;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void incrementHeldYieldingLockCount() {
|
|
+ this.heldYieldingLockCount++;
|
|
+ if (this.heldYieldingLockCount == 1) {
|
|
+ this.updateCanStartYieldingTasks();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void decrementHeldYieldingLockCount() {
|
|
+ this.heldYieldingLockCount--;
|
|
+ if (this.heldYieldingLockCount == 0) {
|
|
+ this.updateCanStartYieldingTasks();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Updates {@link #canStartYieldingTasks} according to {@link #yieldDepth} and {@link #heldYieldingLockCount}.
|
|
+ */
|
|
+ private void updateCanStartYieldingTasks() {
|
|
+ this.canStartYieldingTasks = this.heldYieldingLockCount == 0 && this.yieldDepth < this.maximumYieldDepth;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * This method is based on {@link #signal}, and must be so, or {@link BaseThreadActivation} will get stuck
|
|
+ * while choosing a thread to activate.
|
|
+ *
|
|
+ * @see #signal
|
|
+ */
|
|
+ @SuppressWarnings("RedundantIfStatement")
|
|
+ public boolean isWaitingAndNeedsSignal() {
|
|
+ if (this.isWaiting) {
|
|
+ if (this.isNotActuallyWaitingYet) {
|
|
+ if (!this.skipNextWait) {
|
|
+ return true;
|
|
+ }
|
|
+ return false;
|
|
+ }
|
|
+ if (!this.mayBeStillWaitingButHasBeenSignalled) {
|
|
+ return true;
|
|
+ }
|
|
+ } 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.
|
|
+ * <br>
|
|
+ * 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) {
|
|
+ int oldYieldDepth = this.yieldDepth;
|
|
+ int newYieldDepth = oldYieldDepth + 1;
|
|
+ this.yieldDepth = newYieldDepth;
|
|
+ if (newYieldDepth == maximumYieldDepth) {
|
|
+ this.updateCanStartYieldingTasks();
|
|
+ }
|
|
+ this.runTasksUntil(stopCondition, yieldingLock);
|
|
+ this.yieldDepth = oldYieldDepth;
|
|
+ if (newYieldDepth == maximumYieldDepth) {
|
|
+ this.updateCanStartYieldingTasks();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * This method will keep attempting to find a task to do, and execute it, and if none is found, start waiting
|
|
+ * until 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.
|
|
+ * <br>
|
|
+ * The above is the same as {@link #yieldUntil}, except it may be called in situations that is not 'yielding',
|
|
+ * for instance the endless loop polling tasks performed by a 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.
|
|
+ * <br>
|
|
+ * Using deferral from a {@link TickThread} that is not the correct thread already is highly discouraged
|
|
+ * because yielding from a {@link TickThread} should be avoided whenever possible.
|
|
+ *
|
|
+ * @see TickThreadDeferral
|
|
+ *
|
|
+ * @author Martijn Muijsers under AGPL-3.0
|
|
+ */
|
|
+@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.
|
|
+ * <br>
|
|
+ * The task itself must be non-blocking and may be potentially yielding, but keeping the task yield-free is
|
|
+ * highly preferred because during yielding from a {@link ServerThread}, most other tasks that must be
|
|
+ * executed on a {@link ServerThread} cannot be run.
|
|
+ * <br>
|
|
+ * On an {@link AbstractYieldingThread}, this method yields until the task is completed.
|
|
+ * Like any potentially yielding method, while technically possible to call from any thread, this method should
|
|
+ * generally only be called from a yielding thread, because on any other thread, the thread will block until
|
|
+ * the given task has been completed by the main thread.
|
|
+ * <br>
|
|
+ * If this thread is already an appropriate thread to run the task on, the task is performed on this thread.
|
|
+ *
|
|
+ * @param task The task to run.
|
|
+ * @param span The {@link TaskSpan} of the task.
|
|
+ */
|
|
+ public static <T> T defer(Supplier<T> 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> T deferInternal(@Nullable Runnable runnable, @Nullable Supplier<T> 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<T> future = new CompletableFuture<>();
|
|
+ AbstractYieldingThread yieldingThread = AbstractYieldingThread.currentYieldingThread();
|
|
+ if (yieldingThread != null) {
|
|
+ // Yield until the task completes
|
|
+ BaseTaskQueues.deferredToServerThread.add(() -> {
|
|
+ if (runnable == null) {
|
|
+ //noinspection ConstantConditions
|
|
+ future.complete(supplier.get());
|
|
+ } else {
|
|
+ runnable.run();
|
|
+ future.complete(null);
|
|
+ }
|
|
+ yieldingThread.signal(null);
|
|
+ }, 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.
|
|
+ * <br>
|
|
+ * This has a number of advantages.
|
|
+ * When we require running code that checks whether it is being run on an appropriate {@link TickThread},
|
|
+ * we can run it this way. Since these parts of code are always performed on a {@link TickThread}
|
|
+ * regardless of the thread requesting them to be run, there is no chance of deadlock occurring
|
|
+ * from two different locks being desired in a different order on two of the original threads
|
|
+ * (in fact, if the normally guarded blocks of code are always run exclusively to each other
|
|
+ * when deferred this way, we do not need locks at all).
|
|
+ * <br>
|
|
+ * When deferring from an {@link AbstractYieldingThread},
|
|
+ * we yield to other tasks until the deferred block of code has finished.
|
|
+ * When deferring from another type of thread, the thread is blocked.
|
|
+ * <br>
|
|
+ * Using deferral from a {@link TickThread} that is not the correct thread already is highly discouraged
|
|
+ * because yielding from a {@link TickThread} should be avoided whenever possible.
|
|
+ *
|
|
+ * @author Martijn Muijsers under AGPL-3.0
|
|
+ */
|
|
+@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> T defer(final ServerLevel world, final int chunkX, final int chunkZ, Supplier<T> 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> T defer(final Entity entity, Supplier<T> 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.
|
|
+ * <br>
|
|
+ * The task itself must be non-blocking and may be potentially yielding, but keeping the task yield-free is
|
|
+ * highly preferred because during yielding from a {@link TickThread}, other tasks that must be executed on that
|
|
+ * thread cannot be run.
|
|
+ * <br>
|
|
+ * On a {@link AbstractYieldingThread}, this method yields until the task is completed.
|
|
+ * Like any potentially yielding method, while technically possible to call from any thread, this method should
|
|
+ * generally only be called from a yielding thread, because on any other thread, the thread will block until
|
|
+ * the given task has been completed by the main thread.
|
|
+ * <br>
|
|
+ * If this thread is already an appropriate thread to run the task on, the task is performed on this thread.
|
|
+ *
|
|
+ * @param task The task to run.
|
|
+ * @param span The {@link TaskSpan} of the task.
|
|
+ */
|
|
+ public static <T> T defer(Supplier<T> 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..ac1f646ed846ed6067f77e7526e2bd0a43bf6677
|
|
--- /dev/null
|
|
+++ b/src/main/java/org/galemc/gale/executor/thread/pool/BaseThreadActivation.java
|
|
@@ -0,0 +1,518 @@
|
|
+// 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<BaseThread>[] 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.
|
|
+ * <br>
|
|
+ * When a task is added that is not important enough to warrant doing a full {@link #update},
|
|
+ * calling this method may be skipped.
|
|
+ * <br>
|
|
+ * 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.
|
|
+ * <br>
|
|
+ * 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}.
|
|
+ * <br>
|
|
+ * 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:
|
|
+ * <ul>
|
|
+ * <li>{@link BaseThread#highestTierOfTaskOnStack}</li>
|
|
+ * <li>
|
|
+ * {@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}.
|
|
+ * </li>
|
|
+ * <li>
|
|
+ * {@link BaseThread#canStartYieldingTasks} and the values
|
|
+ * {@link BaseThread#yieldDepth} and {@link BaseThread#heldYieldingLockCount} it depends on.
|
|
+ * //TODO Gale We currently do not call callForUpdate just due to changes in heldYieldingLockCount, do we really have to? That would cause a lot of calls.
|
|
+ * </li>
|
|
+ * </ul>
|
|
+ * This specifically does not include:
|
|
+ * <ul>
|
|
+ * <li>
|
|
+ * The following values that are only used
|
|
+ * in the meta-handling of {@link #update()}, not in the activation of threads:
|
|
+ * <ul>
|
|
+ * <li>{@link #newUpdateCallsReceived}</li>
|
|
+ * <li>{@link #isUpdateOngoing}</li>
|
|
+ * </ul>
|
|
+ * </li>
|
|
+ * <li>
|
|
+ * The following values that are never changed outside of {@link #update()}:
|
|
+ * <ul>
|
|
+ * <li>{@link #numberOfThreadsActiveForTier}</li>
|
|
+ * <li>{@link #threadsWaitingForLockForTier}</li>
|
|
+ * <li>{@link #numberOfThreadsActiveForLowerThanTier}</li>
|
|
+ * <li>{@link #numberOfThreadsActiveForHigherThanTier}</li>
|
|
+ * <li>{@link #numberOfThreadsIntendedToBeActiveForTier}</li>
|
|
+ * </ul>
|
|
+ * </li>
|
|
+ * <li>
|
|
+ * {@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.
|
|
+ * </li>
|
|
+ * </ul>
|
|
+ */
|
|
+ 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.get() && !isUpdateOngoing.getAndSet(true)) {
|
|
+ // Perform an update
|
|
+ do {
|
|
+ try {
|
|
+ /*
|
|
+ * If newUpdateCallsReceived is zero here, it was set to 0 between the check for whether
|
|
+ * it is positive and the setting to true of isUpdateGoing in the while statement below,
|
|
+ * or it was set to 0 between the increment and the subsequent setting to true of isUpdateGoing
|
|
+ * at the start of this method.
|
|
+ */
|
|
+ if (newUpdateCallsReceived.get() > 0) {
|
|
+ update();
|
|
+ }
|
|
+ } finally {
|
|
+ isUpdateOngoing.set(false);
|
|
+ }
|
|
+ /*
|
|
+ If newUpdateCallsReceived is positive here, it was increased between it being set to 0 and
|
|
+ isUpdateOngoing being set to false, so we must repeat.
|
|
+ */
|
|
+ } while (newUpdateCallsReceived.get() > 0 && !isUpdateOngoing.get() && !isUpdateOngoing.getAndSet(true));
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Activates threads as necessary, and computes whether threads must de-activate themselves when they can.
|
|
+ * <br>
|
|
+ * 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 threadIToUpdateYieldPotential = 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;
|
|
+ int yieldPotential = thread.maximumYieldDepth - yieldDepth;
|
|
+ if (threadIToUpdate == -1) {
|
|
+ isBestChoice = true;
|
|
+ } else if (isThreadWaitingForAvailableYieldingLock != threadIToUpdateIsWaitingForAvailableYieldingLock) {
|
|
+ isBestChoice = isThreadWaitingForAvailableYieldingLock;
|
|
+ } else if (threadIToUpdateYieldDepth == 0 && yieldDepth != 0) {
|
|
+ isBestChoice = true;
|
|
+ } else if (yieldDepth != 0) {
|
|
+ if (yieldPotential > threadIToUpdateYieldPotential) {
|
|
+ isBestChoice = true;
|
|
+ } else if (highestTierOfTaskOnStackOrdinalOrLength > threadIToUpdateTierOrdinalOrLength) {
|
|
+ isBestChoice = true;
|
|
+ }
|
|
+ }
|
|
+ if (isBestChoice) {
|
|
+ threadIToUpdate = threadI;
|
|
+ threadIToUpdateIsWaitingForAvailableYieldingLock = isThreadWaitingForAvailableYieldingLock;
|
|
+ threadIToUpdateYieldDepth = yieldDepth;
|
|
+ threadIToUpdateYieldPotential = yieldPotential;
|
|
+ threadIToUpdateTierOrdinalOrLength = highestTierOfTaskOnStackOrdinalOrLength;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ if (threadIToUpdate == -1) {
|
|
+ // No valid thread was found
|
|
+ break;
|
|
+ }
|
|
+ // Check if the thread still seems valid and attempt to activate it
|
|
+ BaseThread thread = threads[threadIToUpdate];
|
|
+ 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}.
|
|
+ * <br>
|
|
+ * This pool intends to keep {@link #targetParallelism} threads active at any time,
|
|
+ * which includes a potentially active {@link ServerThread}.
|
|
+ * <br>
|
|
+ * 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.
|
|
+ * <br><br>
|
|
+ * 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}.
|
|
+ * <br>
|
|
+ * This value is always positive.
|
|
+ * <br>
|
|
+ * The value is currently automatically determined according to the following table:
|
|
+ * <table>
|
|
+ * <tr><th>system cores</th><th>cores spared</th></tr>
|
|
+ * <tr><td>≤ 3</td><td>0</td></tr>
|
|
+ * <tr><td>[4, 14]</td><td>1</td></tr>
|
|
+ * <tr><td>[15, 23]</td><td>2</td></tr>
|
|
+ * <tr><td>[24, 37]</td><td>3</td></tr>
|
|
+ * <tr><td>[38, 54]</td><td>4</td></tr>
|
|
+ * <tr><td>[55, 74]</td><td>5</td></tr>
|
|
+ * <tr><td>[75, 99]</td><td>6</td></tr>
|
|
+ * <tr><td>[100, 127]</td><td>7</td></tr>
|
|
+ * <tr><td>[128, 158]</td><td>8</td></tr>
|
|
+ * <tr><td>[159, 193]</td><td>9</td></tr>
|
|
+ * <tr><td>[194, 232]</td><td>10</td></tr>
|
|
+ * <tr><td>[233, 274]</td><td>11</td></tr>
|
|
+ * <tr><td>≥ 275</td><td>12</td></tr>
|
|
+ * </table>
|
|
+ * Then <code>target parallelism = system cores - cores spared</code>.
|
|
+ * <br>
|
|
+ * 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.
|
|
+ * <br>
|
|
+ * This value is always nonnegative.
|
|
+ * <br>
|
|
+ * This value is currently automatically determined according to the following rule:
|
|
+ * <ul>
|
|
+ * <li>0, if {@link #targetParallelism} = 1</li>
|
|
+ * <li>{@code max(1, floor(2/5 * }{@link #targetParallelism}{@code ))}</li>
|
|
+ * </ul>
|
|
+ * 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}.
|
|
+ * <br>
|
|
+ * 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}.
|
|
+ * <br>
|
|
+ * 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.
|
|
+ * <br>
|
|
+ * 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.
|
|
+ * <br>
|
|
+ * Note that the {@link ServerThread} at index 0 may be null if {@link MinecraftServer#isConstructed} is false.
|
|
+ * <br>
|
|
+ * 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
|