diff --git a/src/main/java/net/gensokyoreimagined/nitori/executor/annotation/thread/AssistThreadOnly.java b/src/main/java/net/gensokyoreimagined/nitori/executor/annotation/thread/AssistThreadOnly.java new file mode 100644 index 0000000..829c829 --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/executor/annotation/thread/AssistThreadOnly.java @@ -0,0 +1,31 @@ +package net.gensokyoreimagined.nitori.executor.annotation.thread; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * An annotation primarily for methods, identifying methods that can only be called on a thread that is an instance + * of {@link AssistThread}. + *
+ * This annotation can also be used on fields or classes, similar to {@link ThreadRestricted}. + *
+ * In a method annotated with {@link AssistThreadOnly}, fields and methods annotated with + * {@link AssistThreadOnly}, {@link BaseThreadOnly} or {@link AnyThreadSafe} may be used. + *
+ * Methods that are annotated with {@link AssistThreadOnly} must never call methods that are annotated with + * {@link PotentiallyBlocking}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +@SuppressWarnings("unused") +@Documented +@Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD}) +public @interface AssistThreadOnly { + + /** + * @see ThreadRestricted#fieldAccess() + */ + Access value() default Access.READ_WRITE; + +} \ No newline at end of file diff --git a/src/main/java/net/gensokyoreimagined/nitori/executor/queue/BaseTaskQueueTier.java b/src/main/java/net/gensokyoreimagined/nitori/executor/queue/BaseTaskQueueTier.java new file mode 100644 index 0000000..e0fa918 --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/executor/queue/BaseTaskQueueTier.java @@ -0,0 +1,114 @@ +package net.gensokyoreimagined.nitori.executor.queue; + +/** + * A tier for {@link AbstractTaskQueue}s, that indicates the priority of the tasks in the task queues. + * Every tier contains a list of the queues that are part of the tier. + * The tiers are in order of priority, from high to low. + * Similarly, the queues for each tier are in the same order of priority. + * The tasks in each queue should also be in order of priority whenever relevant, but usually there + * is no strong difference in priority between tasks in the same queue, so they typically operate as FIFO queues, + * so that the longest waiting task implicitly has the highest priority within the queue. + *
+ * Tasks from queues in the {@link #SERVER} tier can only be run on a {@link ServerThread}. + * Tasks from other tiers can be run on {@link ServerThread}s as well as on {@link AssistThread}s. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public enum BaseTaskQueueTier { + + /** + * A tier for queues that contain tasks that must be executed on a {@link ServerThread}. + *
+ * Some parts of the server can only be safely accessed by one thread at a time. + * If they can not be guarded by a lock (or if this is not desired, + * because if a ticking thread would need to acquire this lock it would block it), + * then these parts of the code are typically deferred to the server thread. + * Based on the current use of the {@link TickThread} class, particularly given the existence of + * {@link TickThread#isTickThreadFor(Entity)} and {@link TickThread#isTickThreadFor(ServerLevel, int, int)}, + * we can deduce that future support for performing some of these actions in parallel is planned. + * In such a case, some server thread tasks may become tasks that must be + * executed on an appropriate {@link TickThread}. + * In that case, the queues below should be changed so that the server thread and any of the + * ticking threads poll from queues that contain tasks appropriate for them. + * For example, {@link BaseTaskQueues#deferredToUniversalTickThread} would be for tasks that can run + * on any ticking thread, and additional queues would need to be added concerning a specific + * subject (like an entity or chunk) with tasks that will be run on whichever ticking thread is the + * ticking thread for that subject at the time of polling. + *
+ * Note that a {@link ServerThread} can only yield to {@link TaskSpan#TINY} tasks in other tiers + * (since there are no higher tiers, and threads can only yield to lower tiers when + * the task yielded to is{@link TaskSpan#TINY}, or other non-yielding tasks in its own tier (since it + * has a {@link BaseThread#maximumYieldDepth} of 1). + * Yielding to other tasks in this same tier is somewhat risky, since this means that the tasks that were + * yielded to must assume that although they are running on the server thread, they may be running at + * some unknown point in execution of the main thread. Therefore, scheduling any non-yielding tasks to + * a queue in this tier must be done with the utmost care that the task cannot disrupt, or be disrupted by, + * the surrounding code that yields to it. + */ + SERVER(new AbstractTaskQueue[]{ + BaseTaskQueues.deferredToServerThread, + BaseTaskQueues.serverThreadTick, + BaseTaskQueues.anyTickScheduledServerThread + }, MinecraftServer.SERVER_THREAD_PRIORITY), + /** + * A tier for queues that contain tasks that are part of ticking, + * to assist the main ticking thread(s) in doing so. + */ + TICK_ASSIST(new AbstractTaskQueue[]{ + BaseTaskQueues.tickAssist + }, Integer.getInteger("gale.thread.priority.tick", 7)), + /** + * A tier for queues that contain general tasks that must be performed at some point in time, + * asynchronously with respect to the {@link ServerThread} and the ticking of the server. + * Execution of + */ + ASYNC(new AbstractTaskQueue[0], Integer.getInteger("gale.thread.priority.async", 6)), + /** + * A tier for queues that contain tasks with the same considerations as {@link #ASYNC}, + * but with a low priority. + */ + LOW_PRIORITY_ASYNC(new AbstractTaskQueue[0], Integer.getInteger("gale.thread.priority.async.low", 3)); + + /** + * Equal to {@link #ordinal()}. + */ + public final int ordinal; + + /** + * The task queues that belong to this tier. + */ + public final AbstractTaskQueue[] taskQueues; + + /** + * The priority for threads that are executing a task from this tier. + *
+ * If a thread yields to other tasks, the priority it will have is always the highest priority of any task + * on its stack. + */ + public final int threadPriority; + + BaseTaskQueueTier(AbstractTaskQueue[] taskQueues, int threadPriority) { + this.ordinal = this.ordinal(); + this.taskQueues = taskQueues; + for (AbstractTaskQueue queue : this.taskQueues) { + queue.setTier(this); + } + this.threadPriority = threadPriority; + } + + /** + * Equal to {@link #values()}. + */ + public static final BaseTaskQueueTier[] VALUES = values(); + + /** + * Equal to {@link #VALUES}{@code .length}. + */ + public static final int length = VALUES.length; + + /** + * Equal to {@link #VALUES} without {@link #SERVER}. + */ + public static final BaseTaskQueueTier[] VALUES_EXCEPT_SERVER = Arrays.stream(VALUES).filter(tier -> tier != SERVER).toList().toArray(new BaseTaskQueueTier[length - 1]); + +} diff --git a/src/main/java/net/gensokyoreimagined/nitori/executor/thread/AssistThread.java b/src/main/java/net/gensokyoreimagined/nitori/executor/thread/AssistThread.java new file mode 100644 index 0000000..51a80d5 --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/executor/thread/AssistThread.java @@ -0,0 +1,69 @@ +package net.gensokyoreimagined.nitori.executor.thread; + +/** + * A thread created by the {@link BaseThreadPool}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public class AssistThread extends BaseThread { + + /** + * The maximum yield depth. While an {@link AssistThread} has a yield depth equal to or greater than this value, + * it can not start more potentially yielding tasks. + */ + public static final int MAXIMUM_YIELD_DEPTH = Integer.getInteger("gale.yield.depth.max", 100); + + /** + * The index of this thread, as needed as an argument to + * {@link BaseThreadPool#getThreadByAssistIndex(int)}. + */ + public final int assistThreadIndex; + + /** + * Must only be called from {@link BaseThreadPool#addAssistThread}. + */ + public AssistThread(int assistThreadIndex) { + super(AssistThread::getCurrentAssistThreadAndRunForever, "Assist Thread " + assistThreadIndex, assistThreadIndex + 1, MAXIMUM_YIELD_DEPTH); + this.assistThreadIndex = assistThreadIndex; + } + + /** + * Causes this thread to loop forever, always attempting to find a task to do, and if none is found, + * registering itself with the places where a relevant task may be added in order to be signalled when + * one is actually added. + */ + @ThisThreadOnly + protected void runForever() { + this.runTasksUntil(null, () -> false, null); + } + + /** + * @return The current thread if it is a {@link AssistThread}, or null otherwise. + */ + @SuppressWarnings("unused") + @AnyThreadSafe + @YieldFree + public static @Nullable AssistThread currentAssistThread() { + return Thread.currentThread() instanceof AssistThread assistThread ? assistThread : null; + } + + /** + * @return Whether the current thread is a {@link AssistThread}. + */ + @SuppressWarnings("unused") + @AnyThreadSafe + @YieldFree + public static boolean isAssistThread() { + return Thread.currentThread() instanceof AssistThread; + } + + /** + * A method that simply acquires the {@link AssistThread} that is the current thread, and calls + * {@link #runForever()} on it. + */ + @BaseThreadOnly + protected static void getCurrentAssistThreadAndRunForever() { + ((AssistThread) Thread.currentThread()).runForever(); + } + +} diff --git a/src/main/java/net/gensokyoreimagined/nitori/executor/thread/BaseThread.java b/src/main/java/net/gensokyoreimagined/nitori/executor/thread/BaseThread.java new file mode 100644 index 0000000..c5cfb97 --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/executor/thread/BaseThread.java @@ -0,0 +1,743 @@ +package net.gensokyoreimagined.nitori.executor.thread; + +import net.gensokyoreimagined.nitori.executor.wrapper.MinecraftServerWrapper; + +/** + * An abstract base class implementing {@link AbstractYieldingThread}, + * that provides implementation that is common between + * {@link TickThread} and {@link AssistThread}. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public abstract class BaseThread extends Thread implements AbstractYieldingThread { + + /** + * The minimum time to wait as the {@link MinecraftServer#serverThread} when performing a timed wait. + * Given in nanoseconds. + * If a timed wait with a lower time is attempted, the wait is not performed at all. + */ + public static final long SERVER_THREAD_WAIT_NANOS_MINIMUM = 10_000; + + /** + * The time to wait as the {@link MinecraftServer#serverThread} during the oversleep phase, if + * there may be delayed tasks. + * Given in nanoseconds. + */ + public static final long SERVER_THREAD_WAIT_NANOS_DURING_OVERSLEEP_WITH_DELAYED_TASKS = 50_000; + + /** + * The index of this thread, as needed as an argument to + * {@link BaseThreadPool#getThreadByBaseIndex(int)}. + */ + public final int baseThreadIndex; + + /** + * The maximum yield depth for this thread, + * which equals 1 for a {@link ServerThread} + * and {@link AssistThread#MAXIMUM_YIELD_DEPTH} for an {@link AssistThread}. + */ + public final int maximumYieldDepth; + + /** + * The number of times this thread holds a {@link YieldingLock}, + * used in {@link #holdsYieldingLock()}. + * + * @see AbstractYieldingThread#incrementHeldYieldingLockCount() + */ + @ThisThreadOnly + public int heldYieldingLockCount = 0; + + /** + * The current yield depth of this thread. + */ + @AnyThreadSafe(Access.READ) @ThisThreadOnly(Access.WRITE) + public volatile int yieldDepth = 0; + + /** + * Whether this thread can currently start yielding tasks with respect to being restricted + * due to {@link #yieldDepth} being at least {@link #maximumYieldDepth}. + *
+ * This is updated using {@link #updateCanStartYieldingTasks()} + * after {@link #yieldDepth} or {@link #heldYieldingLockCount} is changed. + */ + @AnyThreadSafe(Access.READ) @ThisThreadOnly(Access.WRITE) + public volatile boolean canStartYieldingTasks = true; + + /** + * The highest {@link BaseTaskQueueTier} of any task on the yielding execution stack of this thread, + * or null if there is no task being executed on this thread. + */ + @AnyThreadSafe(Access.READ) @ThisThreadOnly(Access.WRITE) + public volatile @Nullable BaseTaskQueueTier highestTierOfTaskOnStack; + + /** + * The {@link BaseTaskQueueTier} that the last non-null return value of {@link #pollTask} was polled from, + * or null if {@link #pollTask} has never been called yet. + */ + @ThisThreadOnly + private @Nullable BaseTaskQueueTier lastPolledTaskTier; + + /** + * The lock to guard this thread's sleeping and waking actions. + */ + private final Lock waitLock = new ReentrantLock(); + + /** + * The condition to wait for a signal, when this thread has to wait for something to do. + */ + private final Condition waitCondition = waitLock.newCondition(); + + /** + * Whether this thread is currently not working on the content of a task, but instead + * attempting to poll a next task to do, checking whether it can accept tasks at all, or + * attempting to acquire a {@link YieldingLock}, or waiting (although the fact that this value is true during + * waiting is irrelevant, because at such a time, {@link #isWaiting} will be true, and this value will no longer + * have any effect due to the implementation of {@link #signal}). + *
+ * This value is used to determine whether to set {@link #skipNextWait} when {@link #signal} is called + * and {@link #isWaiting} is false. + */ + @AnyThreadSafe(Access.READ) @ThisThreadOnly(Access.WRITE) + private volatile boolean isPollingTaskOrCheckingStopCondition = true; + + /** + * Whether this thread should not start waiting for something to do the next time no task could be polled, + * but instead try polling a task again. + */ + @AnyThreadSafe + public volatile boolean skipNextWait = false; + + /** + * Whether this thread is currently waiting for something to do. + *
+ * This is set to true at some point before actually starting to wait in a blocking fashion, + * and set to false at some point after no longer waiting in a blocking fashion. So, at some point, + * this value may be true while the thread is not blocked yet, or anymore. + * Even more so, extra checks for whether the thread should block will be performed in between + * the moment this value is set to true and the moment the thread potentially blocks. This means that if the + * checks fail, this value may be set to true and then false again, without actually ever blocking. + */ + @AnyThreadSafe(Access.READ) @ThisThreadOnly(Access.WRITE) + @Guarded(value = "#waitLock", fieldAccess = Access.WRITE) + public volatile boolean isWaiting = false; + + /** + * Whether {@link #isWaiting} is irrelevant because this thread has already + * been signalled via {@link #signal} to wake up. + */ + @AnyThreadSafe(Access.READ) @ThisThreadOnly(Access.WRITE) + @Guarded(value = "#waitLock", fieldAccess = Access.WRITE) + public volatile boolean mayBeStillWaitingButHasBeenSignalled = false; + + /** + * The {@link YieldingLock} that this thread is waiting for, + * or null if this thread is not waiting for a {@link YieldingLock}. + * This value only has meaning while {@link #isWaiting} is true. + */ + @AnyThreadSafe(Access.READ) @ThisThreadOnly(Access.WRITE) + @Guarded(value = "#waitLock", fieldAccess = Access.WRITE) + public volatile @Nullable YieldingLock lockWaitingFor = null; + + /** + * The value of {@link #lockWaitingFor} during the last wait (a call to {@link Condition#await}) + * or pre-wait check (while {@link #isNotActuallyWaitingYet} is true). + */ + @ThisThreadOnly + private @Nullable YieldingLock lastLockWaitedFor = null; + + /** + * A special flag, used after changing {@link #isWaiting}, when the lock must be temporarily released to + * call {@link BaseThreadActivation#callForUpdate()} (to avoid deadlocks in {@link #signal} calls), + * and we wish the pool to regard this thread as waiting + * (which it will, because {@link #isWaiting} will be true), but we must still + * know not to signal the underlying {@link #waitCondition}, but set {@link #skipNextWait} to true, + * when {@link #signal} is called at some point during the short release of {@link #waitLock}. + */ + public volatile boolean isNotActuallyWaitingYet = false; + + /** + * The last reason this thread was signalled before the current poll attempt, or null if the current + * poll attempt was not preceded by signalling (but by yielding for example). + */ + public volatile @Nullable SignalReason lastSignalReason = null; + + protected BaseThread(Runnable target, String name, int baseThreadIndex, int maximumYieldDepth) { + super(target, name); + this.baseThreadIndex = baseThreadIndex; + this.maximumYieldDepth = maximumYieldDepth; + } + + @Override + public boolean holdsYieldingLock() { + return this.heldYieldingLockCount > 0; + } + + @Override + public void incrementHeldYieldingLockCount() { + this.heldYieldingLockCount++; + if (this.heldYieldingLockCount == 1) { + this.updateCanStartYieldingTasks(); + } + } + + @Override + public void decrementHeldYieldingLockCount() { + this.heldYieldingLockCount--; + if (this.heldYieldingLockCount == 0) { + this.updateCanStartYieldingTasks(); + } + } + + /** + * Updates {@link #canStartYieldingTasks} according to {@link #yieldDepth} and {@link #heldYieldingLockCount}. + */ + private void updateCanStartYieldingTasks() { + this.canStartYieldingTasks = this.heldYieldingLockCount == 0 && this.yieldDepth < this.maximumYieldDepth; + } + + /** + * This method is based on {@link #signal}. + * {@link #signal} must always return true if this method returns true; + * otherwise {@link BaseThreadActivation} will get stuck while choosing a thread to activate. + * + * @see #signal + */ + @SuppressWarnings("RedundantIfStatement") + public boolean isWaitingAndNeedsSignal() { + if (this.isWaiting) { + if (this.isNotActuallyWaitingYet) { + if (!this.skipNextWait) { + return true; + } + return false; + } + if (!this.mayBeStillWaitingButHasBeenSignalled) { + return true; + } + } + return false; + } + + /** + * Yields to tasks: polls and executes tasks while possible and the stop condition is not met. + * The stop condition is met if {@code stopCondition} is not null and returns true, or alternatively, + * if {@code stopCondition} is null, and {@code yieldingLock} is successfully acquired. + * When no tasks can be polled, this thread will block, waiting for either a task that can be executed by this + * thread to become available, or for the {@code yieldingLock}, if given, to be released. + *
+ * Exactly one of {@code stopCondition} and {@code yieldingLock} must be non-null. + */ + public final void yieldUntil(@Nullable Long timeoutTime, @Nullable BooleanSupplier stopCondition, @Nullable YieldingLock yieldingLock) { + int oldYieldDepth = this.yieldDepth; + int newYieldDepth = oldYieldDepth + 1; + this.yieldDepth = newYieldDepth; + if (newYieldDepth == maximumYieldDepth) { + this.updateCanStartYieldingTasks(); + } + this.runTasksUntil(timeoutTime, stopCondition, yieldingLock); + this.yieldDepth = oldYieldDepth; + if (newYieldDepth == maximumYieldDepth) { + this.updateCanStartYieldingTasks(); + } + } + + /** + * This method will keep attempting to find a task to do, and execute it, and if none is found, start waiting + * until the {@code timeoutTime} is reached (which is compared to {@link System#nanoTime}), + * or the thread is signalled by {@link BaseThreadPool} or by a {@link YieldingLock}. + * The loop is broken as soon as the stop condition becomes true, or the given lock is successfully acquired. + *
+ * The above is the same as {@link #yieldUntil}, except it may be called in situations that is not 'yielding', + * for instance the endless loop polling tasks performed by a n{@link AssistThread}. The difference with + * {@link #yieldUntil} is that this method does not increment or decrement things the yield depth of this thread. + *
+ * Exactly one of {@code stopCondition} or {@code yieldingLock} must be non-null. + * + * @see #yieldUntil + */ + @ThisThreadOnly + @PotentiallyYielding("may yield further if an executed task is potentially yielding") + public final void runTasksUntil(@Nullable Long timeoutTime, @Nullable BooleanSupplier stopCondition, @Nullable YieldingLock yieldingLock) { + if (TickThread.isTickThread()) MinecraftServer.THREAD_DEBUG_LOGGER.ifPresent(it -> it.info("running tasks until")); + this.isPollingTaskOrCheckingStopCondition = true; + + /* + Endless loop that attempts to perform a task, and if one is found, tries to perform another again, + but if none is found, starts awaiting such a task to become available, or for the given yielding lock + to be released. + */ + while (true) { + try { + if (timeoutTime != null && System.nanoTime() - timeoutTime >= 0) { + break; + } + if (stopCondition != null) { + if (this == MinecraftServerWrapper.serverThread) { + MinecraftServer.currentManagedBlockStopConditionHasBecomeTrue = false; + } + if (stopCondition.getAsBoolean()) { + if (this == MinecraftServerWrapper.serverThread) { + MinecraftServer.currentManagedBlockStopConditionHasBecomeTrue = true; + } + break; + } + } else { + //noinspection ConstantConditions + if (yieldingLock.tryLock()) { + break; + } + } + } finally { + // Make sure other threads can be signalled for the last waited-for lock again + if (this.lastLockWaitedFor != null) { + this.lastLockWaitedFor.canBeSignalledFor = true; + this.lastLockWaitedFor = null; + } + } + + // If this is the original server thread, update isInSpareTimeAndHaveNoMoreTimeAndNotAlreadyBlocking + if (this == MinecraftServerWrapper.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 == MinecraftServerWrapper.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 == MinecraftServerWrapper.serverThread) { + if (newHighestTier != BaseTaskQueueTier.SERVER) { + newHighestTier = BaseTaskQueueTier.SERVER; + this.highestTierOfTaskOnStack = newHighestTier; + BaseThreadActivation.callForUpdate(); + if (newThreadPriority != newHighestTier.threadPriority) { + newThreadPriority = newHighestTier.threadPriority; + this.setPriority(newThreadPriority); + } + } + MinecraftServer.SERVER.executeMidTickTasks(); // Paper - execute chunk tasks mid tick + } + + // Reset highestTierOfTaskOnStack and the thread priority + if (newHighestTier != highestTierBeforeTask) { + this.highestTierOfTaskOnStack = highestTierBeforeTask; + BaseThreadActivation.callForUpdate(); + if (threadPriorityBeforeTask != newThreadPriority) { + this.setPriority(threadPriorityBeforeTask); + } + } + + this.isPollingTaskOrCheckingStopCondition = true; + continue; + + } + + /* + If no task that can be started by this thread was found, wait for a task that we are allowed + to poll to become available (when that happens, the BaseThreadPool will signal this thread), + or for the given yielding lock to be released. This is the only time we should ever block inside + a potentially yielding procedure. + */ + this.waitUntilSignalled(timeoutTime, yieldingLock); + + } + + this.isPollingTaskOrCheckingStopCondition = false; + + /* + If the thread was signalled for another reason than the lock, but we acquired the lock instead, + another thread should be signalled for that reason. + */ + SignalReason lastSignalReason = this.lastSignalReason; + if (lastSignalReason != null && yieldingLock != null && lastSignalReason != SignalReason.YIELDING_LOCK) { + BaseThreadActivation.callForUpdate(); + } + + } + + /** + * @see #pollTask() + */ + @ThisThreadOnly + @YieldFree + private @Nullable Runnable pollTaskFromTier(BaseTaskQueueTier tier, boolean tinyOnly) { + for (var queue : tier.taskQueues) { + // Check whether we can not yield to the queue, if we are yielding + boolean canQueueBeYieldedTo = queue.canBeYieldedTo(); + if (!canQueueBeYieldedTo && this.yieldDepth > 0) { + continue; + } + Runnable task = tinyOnly ? queue.pollTiny(this) : queue.poll(this); + if (task != null) { + this.lastPolledTaskTier = tier; + return task; + } + /* + Check if the tier has run out of tasks for a span, + in order to update BaseThreadActivation#thereMayBeTasks. + */ + for (int spanI = 0; spanI < TaskSpan.length; spanI++) { + TaskSpan span = TaskSpan.VALUES[spanI]; + if (queue.canHaveTasks(span)) { + int oldTasks = BaseThreadActivation.thereMayBeTasks[tier.ordinal][spanI][canQueueBeYieldedTo ? 1 : 0].get(); + if (oldTasks > 0) { + if (!queue.hasTasks(span)) { + boolean tierHasNoTasksForSpan = true; + for (AbstractTaskQueue otherTierQueue : tier.taskQueues) { + // We already know there are no tasks in this queue + if (otherTierQueue == queue) { + continue; + } + if (otherTierQueue.hasTasks(span)) { + tierHasNoTasksForSpan = false; + break; + } + } + if (tierHasNoTasksForSpan) { + // Set thereMayBeTasks to false, but only if it did not change in the meantime + BaseThreadActivation.thereMayBeTasks[tier.ordinal][spanI][canQueueBeYieldedTo ? 1 : 0].compareAndSet(oldTasks, 0); + } + } + } + } + } + } + return null; + } + + /** + * Polls a task from any queue this thread can currently poll from, and returns it. + * Polling potentially yielding tasks is attempted before yield-free tasks. + * + * @return The task that was polled, or null if no task was found. + */ + @ThisThreadOnly + @YieldFree + private @Nullable Runnable pollTask() { + /* + * If this is a server thread, poll from SERVER, and poll tiny tasks from other tiers. + * Note that when polling on the ServerThread, we do not check whether we would be allowed to do so + * by the BaseThreadPool, as we consider keeping the ServerThread in the Thread.State.RUNNABLE state for + * as long as possible to be more important than the off-chance of for example starting a TINY ASYNC task + * on the server thread while no ASYNC tasks are allowed to be polled by other threads at the moment. + */ + if (this instanceof ServerThread) { + // Poll from the SERVER queues + Runnable task = this.pollTaskFromTier(BaseTaskQueueTier.SERVER, false); + if (task != null) { + return task; + } + // Poll tiny tasks from other tiers + for (var tier : BaseTaskQueueTier.VALUES_EXCEPT_SERVER) { + task = this.pollTaskFromTier(tier, true); + if (task != null) { + return task; + } + } + // We failed to poll any task + return null; + } + // If this is not a server thread, poll from all queues except SERVER + for (var tier : BaseTaskQueueTier.VALUES_EXCEPT_SERVER) { + /* + Make sure that we are allowed to poll from the tier, according to the presence of an excess number of + threads working on tasks from that tier during the last BaseThreadActivation#update call. + In the case this check's result is too optimistic, and a task is started when ideally it wouldn't have been, + then so be it - it is not terrible. Whenever this happens, enough threads will surely be allocated + by the BaseThreadPool for the task tier that is more in demand anyway, so it does not matter much. + In the case this check's result is too pessimistic, the polling fails and this thread will start to sleep, + but before doing this, will make a call to BaseThreadActivation#callForUpdate that re-activated this + thread if necessary, so no harm is done. + In the case this check causes this thread to go to sleep, the call to BaseThreadActivation#callForUpdate + while isWaiting is true will make sure the BaseThreadPool has the ability to correctly activate a + different thread (that is able to start tasks of a higher tier) if needed. + Here, we do not even make an exception for TINY tasks, since there may already be ongoing avoidable + context-switching due to excess threads that we can solve by letting this thread go to sleep. + */ + if (tier.ordinal < BaseThreadActivation.tierInExcessOrdinal) { + /* + Tasks of a certain tier may yield to tasks of the same or a higher + tier, and they may also yield to tiny tasks of a lower tier. + */ + var tierYieldingFrom = this.highestTierOfTaskOnStack; + Runnable task = this.pollTaskFromTier(tier, tierYieldingFrom != null && tier.ordinal > tierYieldingFrom.ordinal); + if (task != null) { + return task; + } + } + } + // We failed to poll any task + return null; + } + + /** + * Starts waiting on something to do. + * + * @param timeoutTime The maximum time to wait until (compared to {@link System#nanoTime}). + * @param yieldingLock A {@link YieldingLock} to register with, or null if this thread is not waiting for + * a yielding lock. + */ + @ThisThreadOnly + @PotentiallyBlocking + private void waitUntilSignalled(@Nullable Long timeoutTime, @Nullable YieldingLock yieldingLock) { + + // Remember whether we registered to wait with the lock, to unregister later + // Register this thread with the lock if necessary + boolean registeredAsWaitingWithLock = false; + if (yieldingLock != null) { + // No point in registering if we're not going to wait anyway + if (!this.skipNextWait) { + yieldingLock.incrementWaitingThreads(); + registeredAsWaitingWithLock = true; + } + } + + /* + Remember whether we changed anything that requires a BaseThreadPool#update call + (after the last call to that method). + */ + boolean mustCallPoolUpdateAtEnd = false; + + /* + If we cannot acquire the lock, we can assume this thread is being signalled, + so there is no reason to start waiting. + */ + waitWithLock: if (this.waitLock.tryLock()) { + try { + + // If it was set that this thread should skip the wait in the meantime, skip it + if (this.skipNextWait) { + break waitWithLock; + } + + // Mark this thread as waiting + this.lockWaitingFor = yieldingLock; + this.mayBeStillWaitingButHasBeenSignalled = false; + this.isWaiting = true; + // But actually we are not waiting yet, signal has no effect yet during the next short lock release + this.isNotActuallyWaitingYet = true; + + } finally { + this.waitLock.unlock(); + } + + // Update the pool + BaseThreadActivation.callForUpdate(); + + /* + If we cannot acquire the lock, we can assume this thread is being signalled, + so there is no reason to start waiting. + */ + if (this.waitLock.tryLock()) { + try { + + // We passed the short lock release + this.isNotActuallyWaitingYet = false; + + // If it was set that this thread should skip the wait in the meantime, skip it + if (this.skipNextWait) { + this.isWaiting = false; + this.lastLockWaitedFor = this.lockWaitingFor; + this.lockWaitingFor = null; + mustCallPoolUpdateAtEnd = true; + break waitWithLock; + } + + // Wait + try { + + // -1 indicates to not use a timeout (this value is not later set to any other negative value) + long waitForNanos = -1; + if (timeoutTime != null) { + waitForNanos = Math.max(timeoutTime - System.nanoTime(), SERVER_THREAD_WAIT_NANOS_MINIMUM); + } else { + /* + Check if we should wait with a tick-based timeout: + this only happens if this thread is the server thread, in + which case we do not want to wait past the start of the next tick. + */ + if (this == MinecraftServerWrapper.serverThread) { + if (MinecraftServer.isWaitingUntilNextTick) { + /* + During waiting until the next tick, we wait until the next tick start. + If it already passed, we do not have to use a timeout, because we will be notified + when the stop condition becomes true. + */ + waitForNanos = MinecraftServer.nextTickStartNanoTime - System.nanoTime(); + if (waitForNanos < 0) { + waitForNanos = -1; + } + } else if (MinecraftServer.SERVER.isOversleep) { + /* + During this phase, MinecraftServer#mayHaveDelayedTasks() is checked, and we may not + be notified when it changes. Therefore, if the next tick start has not passed, we will + wait until then, but if it has, we wait for a short interval to make sure we keep + checking the stop condition (but not for longer than until the last time we can be + executing extra delayed tasks). + */ + waitForNanos = MinecraftServer.nextTickStartNanoTime - System.nanoTime(); + if (waitForNanos < 0) { + waitForNanos = Math.min(Math.max(0, MinecraftServer.delayedTasksMaxNextTickNanoTime - System.nanoTime()), SERVER_THREAD_WAIT_NANOS_DURING_OVERSLEEP_WITH_DELAYED_TASKS); + } + } + } + } + if (waitForNanos >= 0) { + // Set the last signal reason to null in case the timeout elapses without a signal + this.lastSignalReason = null; + // Skip if the time is too short + if (waitForNanos >= SERVER_THREAD_WAIT_NANOS_MINIMUM) { + //noinspection ResultOfMethodCallIgnored + this.waitCondition.await(waitForNanos, TimeUnit.NANOSECONDS); + } + } else { + /* + If we did not wait with a timeout, wait indefinitely. If this thread is the server thread, + and the intended start time of the next tick has already passed, but the stop condition to stop + running tasks is still not true, this thread must be signalled when a change in conditions causes + the stop condition to become true. + */ + this.waitCondition.await(); + } + + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + + // Unmark this thread as waiting + this.isWaiting = false; + this.lastLockWaitedFor = this.lockWaitingFor; + this.lockWaitingFor = null; + mustCallPoolUpdateAtEnd = true; + + } finally { + this.waitLock.unlock(); + } + } + + } + + // Unregister this thread from the lock if necessary + if (registeredAsWaitingWithLock) { + yieldingLock.decrementWaitingThreads(); + } + + // Reset skipping the next wait + this.skipNextWait = false; + + // Update the pool if necessary + if (mustCallPoolUpdateAtEnd) { + BaseThreadActivation.callForUpdate(); + } + + } + + /** + * An auxiliary method for exclusive use in {@link #signal}, that marks the {@link YieldingLock} + * that this thread is waiting for as having been signalled for, so that no other threads + * are also signalled for it. + *
+ * This must be called when {@link #signal} returns true, and must be called before any other + * actions relating to the signalling of this thread are performed. + */ + private void markLockWaitingForAsSignalledFor() { + @Nullable YieldingLock lockWaitingFor = this.lockWaitingFor; + if (lockWaitingFor != null) { + lockWaitingFor.canBeSignalledFor = false; + } + } + + /** + * Signals this thread to wake up, or if it was not sleeping but attempting to poll a task: + * to not go to sleep the next time no task could be polled, and instead try polling a task again. + * + * @param reason The reason why this thread was signalled, or null if it is irrelevant (e.g. when the signal + * will never need to be repeated because there is only thread waiting for this specific event + * to happen). + * @return Whether this thread was sleeping before, and has woken up now, + * or whether {@link #skipNextWait} was set to true. + */ + @AnyThreadSafe + @YieldFree + public final boolean signal(@Nullable SignalReason reason) { + while (!this.waitLock.tryLock()) { // TODO Gale use a wait-free system here by using a sort of leave-a-message-at-the-door Atomic class system + Thread.onSpinWait(); + } + try { + if (this.isWaiting) { + if (this.isNotActuallyWaitingYet) { + if (!this.skipNextWait) { + this.markLockWaitingForAsSignalledFor(); + this.lastSignalReason = reason; + this.skipNextWait = true; + return true; + } + return false; + } + if (!this.mayBeStillWaitingButHasBeenSignalled) { + this.markLockWaitingForAsSignalledFor(); + this.lastSignalReason = reason; + this.mayBeStillWaitingButHasBeenSignalled = true; + this.waitCondition.signal(); + return true; + } + } else if (this.isPollingTaskOrCheckingStopCondition) { + if (!this.skipNextWait) { + this.markLockWaitingForAsSignalledFor(); + this.lastSignalReason = reason; + this.skipNextWait = true; + return true; + } + } + return false; + } finally { + this.waitLock.unlock(); + } + } + + /** + * @return The current thread if it is a {@link BaseThread}, or null otherwise. + */ + @SuppressWarnings("unused") + @AnyThreadSafe + @YieldFree + public static @Nullable BaseThread currentBaseThread() { + return Thread.currentThread() instanceof BaseThread baseThread ? baseThread : null; + } + + /** + * @return Whether the current thread is a {@link BaseThread}. + */ + @SuppressWarnings("unused") + @AnyThreadSafe + @YieldFree + public static boolean isBaseThread() { + return Thread.currentThread() instanceof BaseThread; + } + +} diff --git a/src/main/java/net/gensokyoreimagined/nitori/executor/thread/OriginalServerThread.java b/src/main/java/net/gensokyoreimagined/nitori/executor/thread/OriginalServerThread.java new file mode 100644 index 0000000..474d108 --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/executor/thread/OriginalServerThread.java @@ -0,0 +1,18 @@ +package net.gensokyoreimagined.nitori.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/net/gensokyoreimagined/nitori/executor/thread/ServerThread.java b/src/main/java/net/gensokyoreimagined/nitori/executor/thread/ServerThread.java new file mode 100644 index 0000000..62e35c4 --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/executor/thread/ServerThread.java @@ -0,0 +1,54 @@ +package net.gensokyoreimagined.nitori.executor.thread; + +import io.papermc.paper.util.TickThread; +import net.gensokyoreimagined.nitori.executor.wrapper.MinecraftServerWrapper; +import net.gensokyoreimagined.nitori.mixin.virtual_thread.accessors.MixinMinecraftServer; +import net.gensokyoreimagined.nitori.mixin.virtual_thread.accessors.MixinWatchdogThread; +import net.minecraft.server.MinecraftServer; +import org.gradle.internal.impldep.com.esotericsoftware.kryo.NotNull; +import org.spigotmc.WatchdogThread; + +import javax.annotation.Nullable; + +/** + * 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 Thread getInstance() { + if (((MixinMinecraftServer) MinecraftServerWrapper.SERVER).hasStopped()) { + if (((MixinMinecraftServer) MinecraftServerWrapper.SERVER).shutdownThread() == MixinWatchdogThread.getInstance()) { + return MixinWatchdogThread.getInstance(); + } + } + + return MinecraftServerWrapper.serverThread; + } + + /** + * @return The same value as {@link #getInstance()} if {@link MinecraftServer#isConstructed} is true, + * or null otherwise. + */ + public static @Nullable Thread getInstanceIfConstructed() { + return MinecraftServerWrapper.isConstructed ? getInstance() : null; + } + +} diff --git a/src/main/java/net/gensokyoreimagined/nitori/executor/thread/pool/BaseThreadPool.java b/src/main/java/net/gensokyoreimagined/nitori/executor/thread/pool/BaseThreadPool.java new file mode 100644 index 0000000..802260c --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/executor/thread/pool/BaseThreadPool.java @@ -0,0 +1,208 @@ +// Taken from https://github.com/GaleMC/Gale/blob/ver/1.19.4/patches/server/0151-Base-thread-pool.patch (separate license) + +package net.gensokyoreimagined.nitori.executor.thread.pool; + +/** + * A pool of threads that can perform tasks to assist the current {@link ServerThread}. These tasks can be of + * different {@linkplain BaseTaskQueueTier tiers}. + *
+ * This pool intends to keep {@link #targetParallelism} threads active at any time, + * which includes a potentially active {@link ServerThread}. + *
+ * As such, this pool is closely intertwined with the {@link ServerThread}. This pool can not control the + * {@link ServerThread} in any way, but it is responsible for signalling the {@link ServerThread} when tasks become + * available in a {@link BaseTaskQueueTier#SERVER} task queue, and for listening for when the {@link ServerThread} + * becomes (in)active in order to update the number of active {@link AssistThread}s accordingly. + *

+ * Updates to the threads in this pool are done in a lock-free manner that attempts to do the right thing with + * the volatile information that is available. In some cases, this may cause a thread to be woken up when it + * should not have been, and so on, but the updates being lock-free is more significant than the updates being + * optimal in a high-contention environment. The environment is not expected to have high enough contention for + * this to have much of an impact. Additionally, the suboptimalities in updates are always optimistic in terms of + * making/keeping threads active rather than inactive, and can not a situation where a thread was intended + * to be active, but ends but not being active. + * + * @author Martijn Muijsers under AGPL-3.0 + */ +public final class BaseThreadPool { + + private BaseThreadPool() {} + + public static final String targetParallelismEnvironmentVariable = "gale.threads.target"; + public static final String maxUndisturbedLowerTierThreadCountEnvironmentVariable = "gale.threads.undisturbed"; + + /** + * The target number of threads that will be actively in use by this pool, + * which includes a potentially active {@link ServerThread}. + *
+ * This value is always positive. + *
+ * The value is currently automatically determined according to the following table: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
system threadsthreads spared
≤ 30
[4, 14]1
[15, 23]2
[24, 37]3
[38, 54]4
[55, 74]5
[75, 99]6
[100, 127]7
[128, 158]8
[159, 193]9
[194, 232]10
[233, 274]11
≥ 27512
+ * Then target parallelism = system threads - threads spared. + *
+ * The computed value above can be overridden using the {@link #targetParallelismEnvironmentVariable}. + */ + public static final int targetParallelism; + static { + int parallelismByEnvironmentVariable = Integer.getInteger(targetParallelismEnvironmentVariable, -1); + int targetParallelismBeforeSetAtLeastOne; + if (parallelismByEnvironmentVariable >= 0) { + targetParallelismBeforeSetAtLeastOne = parallelismByEnvironmentVariable; + } else { + int systemThreads = Runtime.getRuntime().availableProcessors(); + int threadsSpared; + if (systemThreads <= 3) { + threadsSpared = 0; + } else if (systemThreads <= 14) { + threadsSpared = 1; + } else if (systemThreads <= 23) { + threadsSpared = 2; + } else if (systemThreads <= 37) { + threadsSpared = 3; + } else if (systemThreads <= 54) { + threadsSpared = 4; + } else if (systemThreads <= 74) { + threadsSpared = 5; + } else if (systemThreads <= 99) { + threadsSpared = 6; + } else if (systemThreads <= 127) { + threadsSpared = 7; + } else if (systemThreads <= 158) { + threadsSpared = 8; + } else if (systemThreads <= 193) { + threadsSpared = 9; + } else if (systemThreads <= 232) { + threadsSpared = 10; + } else if (systemThreads <= 274) { + threadsSpared = 11; + } else { + threadsSpared = 12; + } + targetParallelismBeforeSetAtLeastOne = systemThreads - threadsSpared; + } + targetParallelism = Math.max(1, targetParallelismBeforeSetAtLeastOne); + } + + /** + * The maximum number of threads to be executing tasks, that only have tasks on their thread that are strictly + * below a certain tier, before a thread wishing to execute such tasks gets activated regardless. + * If this threshold of lower tier threads is not exceeded, activating a thread to execute a higher tier task + * will be delayed until one of the active threads finishes execution of their stack or blocks for another + * reason. + *
+ * This value is always nonnegative. + *
+ * This value is currently automatically determined according to the following rule: + * + * The computed value above can be overridden using the {@link #maxUndisturbedLowerTierThreadCountEnvironmentVariable}. + */ + public static final int maxUndisturbedLowerTierThreadCount; + static { + int maxUndisturbedLowerTierThreadCountByEnvironmentVariable = Integer.getInteger(maxUndisturbedLowerTierThreadCountEnvironmentVariable, -1); + maxUndisturbedLowerTierThreadCount = maxUndisturbedLowerTierThreadCountByEnvironmentVariable >= 0 ? maxUndisturbedLowerTierThreadCountByEnvironmentVariable : targetParallelism == 1 ? 0 : Math.max(1, targetParallelism * 2 / 5); + } + + /** + * An array of the {@link AssistThread}s in this pool, indexed by their {@link AssistThread#assistThreadIndex}. + *
+ * This field must only ever be changed from within {@link #addAssistThread}. + */ + private static volatile AssistThread[] assistThreads = new AssistThread[0]; + + /** + * An array of the {@link BaseThread}s in this pool, indexed by their {@link BaseThread#baseThreadIndex}. + *
+ * This field must not be referenced anywhere outside {@link #addAssistThread} or {@link #getBaseThreads()}: + * it only holds the last computed value. + */ + private static volatile @Nullable BaseThread @NotNull [] lastComputedBaseThreads = new BaseThread[1]; + + /** + * Creates a new {@link AssistThread}, adds it to this pool and starts it. + *
+ * Must only be called from within {@link BaseThreadActivation#update()} while + * {@link BaseThreadActivation#updateOngoingOnThread} is not null. + */ + public static void addAssistThread() { + int oldThreadsLength = assistThreads.length; + int newThreadsLength = oldThreadsLength + 1; + // Expand the thread array + AssistThread[] newAssistThreads = Arrays.copyOf(assistThreads, newThreadsLength); + // Create the new thread + AssistThread newThread = newAssistThreads[oldThreadsLength] = new AssistThread(oldThreadsLength); + // Save the new thread array + assistThreads = newAssistThreads; + // Update the assist threads in baseThreads + @SuppressWarnings("NonAtomicOperationOnVolatileField") + BaseThread[] newLastComputedBaseThreads = lastComputedBaseThreads = Arrays.copyOf(lastComputedBaseThreads, newThreadsLength + 1); + newLastComputedBaseThreads[newThreadsLength] = newThread; + // Start the thread + newThread.start(); + MinecraftServer.THREAD_DEBUG_LOGGER.ifPresent(it -> it.info("Added assist thread " + newAssistThreads.length)); + } + + /** + * The {@link BaseThread}s ({@link ServerThread}s and {@link AssistThread}s) in this thread pool, + * specifically for the purpose of easy iteration. + *
+ * Note that the {@link ServerThread} at index 0 may be null if {@link MinecraftServer#isConstructed} is false. + *
+ * Must only be called from within {@link BaseThreadActivation#update()} while + * {@link BaseThreadActivation#updateOngoingOnThread} is not null. + */ + 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]; + } + +} \ No newline at end of file diff --git a/src/main/java/net/gensokyoreimagined/nitori/executor/wrapper/MinecraftServerWrapper.java b/src/main/java/net/gensokyoreimagined/nitori/executor/wrapper/MinecraftServerWrapper.java new file mode 100644 index 0000000..9eb648b --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/executor/wrapper/MinecraftServerWrapper.java @@ -0,0 +1,10 @@ +package net.gensokyoreimagined.nitori.executor.wrapper; + +import net.minecraft.server.MinecraftServer; + +public class MinecraftServerWrapper { + // Named to make replacement of Gale patches easier (replace MinecraftServer.serverThread with MinecraftServerWrapper.serverThread) + public static Thread serverThread; + public static MinecraftServer SERVER; + public static boolean isConstructed; +} diff --git a/src/main/java/net/gensokyoreimagined/nitori/mixin/virtual_thread/accessors/MixinMinecraftServer.java b/src/main/java/net/gensokyoreimagined/nitori/mixin/virtual_thread/accessors/MixinMinecraftServer.java new file mode 100644 index 0000000..f367e2c --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/mixin/virtual_thread/accessors/MixinMinecraftServer.java @@ -0,0 +1,35 @@ +package net.gensokyoreimagined.nitori.mixin.virtual_thread.accessors; + +import com.llamalad7.mixinextras.sugar.Local; +import net.gensokyoreimagined.nitori.executor.thread.OriginalServerThread; +import net.gensokyoreimagined.nitori.executor.wrapper.MinecraftServerWrapper; +import net.minecraft.server.MinecraftServer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(MinecraftServer.class) +public interface MixinMinecraftServer { + @Inject(method = "", at = @At("RETURN")) + private void onConstructed(CallbackInfo ci, @Local(argsOnly = true) Thread thread) { + MinecraftServerWrapper.SERVER = (MinecraftServer) this; + MinecraftServerWrapper.isConstructed = true; + + if (thread instanceof OriginalServerThread) { + MinecraftServerWrapper.serverThread = (OriginalServerThread) thread; + return; + } + + throw new AssertionError("Type of serverThread is not OriginalServerThread!"); + } + + @Accessor("hasStopped") + boolean hasStopped(); + + @Accessor("shutdownThread") + Thread shutdownThread(); + + @Accessor("") +} diff --git a/src/main/java/net/gensokyoreimagined/nitori/mixin/virtual_thread/accessors/MixinWatchdogThread.java b/src/main/java/net/gensokyoreimagined/nitori/mixin/virtual_thread/accessors/MixinWatchdogThread.java new file mode 100644 index 0000000..48b240c --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/mixin/virtual_thread/accessors/MixinWatchdogThread.java @@ -0,0 +1,13 @@ +package net.gensokyoreimagined.nitori.mixin.virtual_thread.accessors; + +import org.spigotmc.WatchdogThread; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(WatchdogThread.class) +public interface MixinWatchdogThread { + @Accessor("instance") + static WatchdogThread getInstance() { + throw new AssertionError("MixinWatchdogThread was not transformed!"); + } +} diff --git a/src/main/java/net/gensokyoreimagined/nitori/mixin/virtual_thread/DirectVirtualThreadService.java b/src/main/java/net/gensokyoreimagined/nitori/virtual_thread/DirectVirtualThreadService.java similarity index 97% rename from src/main/java/net/gensokyoreimagined/nitori/mixin/virtual_thread/DirectVirtualThreadService.java rename to src/main/java/net/gensokyoreimagined/nitori/virtual_thread/DirectVirtualThreadService.java index cfd6fdf..86fe01f 100644 --- a/src/main/java/net/gensokyoreimagined/nitori/mixin/virtual_thread/DirectVirtualThreadService.java +++ b/src/main/java/net/gensokyoreimagined/nitori/virtual_thread/DirectVirtualThreadService.java @@ -1,6 +1,6 @@ // Gale - virtual thread support -package net.gensokyoreimagined.nitori.mixin.virtual_thread; +package net.gensokyoreimagined.nitori.virtual_thread; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/net/gensokyoreimagined/nitori/mixin/virtual_thread/ReflectionVirtualThreadService.java b/src/main/java/net/gensokyoreimagined/nitori/virtual_thread/ReflectionVirtualThreadService.java similarity index 97% rename from src/main/java/net/gensokyoreimagined/nitori/mixin/virtual_thread/ReflectionVirtualThreadService.java rename to src/main/java/net/gensokyoreimagined/nitori/virtual_thread/ReflectionVirtualThreadService.java index a5b0cf6..b2fd6b2 100644 --- a/src/main/java/net/gensokyoreimagined/nitori/mixin/virtual_thread/ReflectionVirtualThreadService.java +++ b/src/main/java/net/gensokyoreimagined/nitori/virtual_thread/ReflectionVirtualThreadService.java @@ -1,6 +1,6 @@ // Gale - virtual thread support -package net.gensokyoreimagined.nitori.mixin.virtual_thread; +package net.gensokyoreimagined.nitori.virtual_thread; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/net/gensokyoreimagined/nitori/mixin/virtual_thread/VirtualThreadService.java b/src/main/java/net/gensokyoreimagined/nitori/virtual_thread/VirtualThreadService.java similarity index 98% rename from src/main/java/net/gensokyoreimagined/nitori/mixin/virtual_thread/VirtualThreadService.java rename to src/main/java/net/gensokyoreimagined/nitori/virtual_thread/VirtualThreadService.java index 903f803..132de81 100644 --- a/src/main/java/net/gensokyoreimagined/nitori/mixin/virtual_thread/VirtualThreadService.java +++ b/src/main/java/net/gensokyoreimagined/nitori/virtual_thread/VirtualThreadService.java @@ -1,6 +1,6 @@ // Gale - virtual thread support -package net.gensokyoreimagined.nitori.mixin.virtual_thread; +package net.gensokyoreimagined.nitori.virtual_thread; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/src/main/java/net/gensokyoreimagined/nitori/mixin/virtual_thread/package-info.java b/src/main/java/net/gensokyoreimagined/nitori/virtual_thread/package-info.java similarity index 66% rename from src/main/java/net/gensokyoreimagined/nitori/mixin/virtual_thread/package-info.java rename to src/main/java/net/gensokyoreimagined/nitori/virtual_thread/package-info.java index 122a8b9..9c0c7e7 100644 --- a/src/main/java/net/gensokyoreimagined/nitori/mixin/virtual_thread/package-info.java +++ b/src/main/java/net/gensokyoreimagined/nitori/virtual_thread/package-info.java @@ -1,4 +1,4 @@ -package net.gensokyoreimagined.nitori.mixin.virtual_thread; +package net.gensokyoreimagined.nitori.virtual_thread; /* Original patch: diff --git a/src/main/resources/mixins.core.json b/src/main/resources/mixins.core.json index da5967f..4f71c78 100755 --- a/src/main/resources/mixins.core.json +++ b/src/main/resources/mixins.core.json @@ -92,6 +92,8 @@ "world.portal_checks.DisablePortalChecksMixin", "world.blending.BlendMixin", "world.farmland.FarmlandBlockMixin", - "world.biome_access.BiomeAccessMixin" + "world.biome_access.BiomeAccessMixin", + "virtual_thread.accessors.MixinMinecraftServer", + "virtual_thread.accessors.MixinWatchdogThread" ] }