W.I.P. implement virtual threading

This commit is contained in:
DoggySazHi
2024-07-20 23:23:14 -07:00
parent 663483c5e6
commit 566eebfe5a
15 changed files with 1302 additions and 5 deletions

View File

@@ -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}.
* <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;
}

View File

@@ -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.
* <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.
* <br>
* 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.
* <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]);
}

View File

@@ -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();
}
}

View File

@@ -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}.
* <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;
/**
* 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.
* <br>
* 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.
* <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.
* <br>
* 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.
* <br>
* 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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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}.
* <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 threads</th><th>threads spared</th></tr>
* <tr><td>&#8804; 3</td><td>0</td></tr>
* <tr><td>[4, 14]</td><td>1</td></tr>
* <tr><td>[15, 23]</td><td>2</td></tr>
* <tr><td>[24, 37]</td><td>3</td></tr>
* <tr><td>[38, 54]</td><td>4</td></tr>
* <tr><td>[55, 74]</td><td>5</td></tr>
* <tr><td>[75, 99]</td><td>6</td></tr>
* <tr><td>[100, 127]</td><td>7</td></tr>
* <tr><td>[128, 158]</td><td>8</td></tr>
* <tr><td>[159, 193]</td><td>9</td></tr>
* <tr><td>[194, 232]</td><td>10</td></tr>
* <tr><td>[233, 274]</td><td>11</td></tr>
* <tr><td>&#8805; 275</td><td>12</td></tr>
* </table>
* Then <code>target parallelism = system threads - threads 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 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.
* <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#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.
* <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#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];
}
}

View File

@@ -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;
}

View File

@@ -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 = "<init>", 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("")
}

View File

@@ -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!");
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -1,4 +1,4 @@
package net.gensokyoreimagined.nitori.mixin.virtual_thread;
package net.gensokyoreimagined.nitori.virtual_thread;
/*
Original patch:

View File

@@ -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"
]
}