mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-06-27 15:43:11 +00:00
Merge branch 'feature/split-computer-thread' into mc-1.20.x
This commit is contained in:
commit
e3ced84885
@ -112,7 +112,9 @@ SPDX-License-Identifier: MPL-2.0
|
|||||||
<module name="LambdaParameterName" />
|
<module name="LambdaParameterName" />
|
||||||
<module name="LocalFinalVariableName" />
|
<module name="LocalFinalVariableName" />
|
||||||
<module name="LocalVariableName" />
|
<module name="LocalVariableName" />
|
||||||
<module name="MemberName" />
|
<module name="MemberName">
|
||||||
|
<property name="format" value="^\$?[a-z][a-zA-Z0-9]*$" />
|
||||||
|
</module>
|
||||||
<module name="MethodName">
|
<module name="MethodName">
|
||||||
<property name="format" value="^(computercraft\$)?[a-z][a-zA-Z0-9]*$" />
|
<property name="format" value="^(computercraft\$)?[a-z][a-zA-Z0-9]*$" />
|
||||||
</module>
|
</module>
|
||||||
@ -122,7 +124,7 @@ SPDX-License-Identifier: MPL-2.0
|
|||||||
</module>
|
</module>
|
||||||
<module name="ParameterName" />
|
<module name="ParameterName" />
|
||||||
<module name="StaticVariableName">
|
<module name="StaticVariableName">
|
||||||
<property name="format" value="^[a-z][a-zA-Z0-9]*|CAPABILITY(_[A-Z_]+)?$" />
|
<property name="format" value="^[a-z][a-zA-Z0-9]*$" />
|
||||||
</module>
|
</module>
|
||||||
<module name="TypeName" />
|
<module name="TypeName" />
|
||||||
|
|
||||||
|
@ -8,8 +8,9 @@ import dan200.computercraft.api.lua.ILuaAPIFactory;
|
|||||||
import dan200.computercraft.core.asm.GenericMethod;
|
import dan200.computercraft.core.asm.GenericMethod;
|
||||||
import dan200.computercraft.core.asm.LuaMethodSupplier;
|
import dan200.computercraft.core.asm.LuaMethodSupplier;
|
||||||
import dan200.computercraft.core.asm.PeripheralMethodSupplier;
|
import dan200.computercraft.core.asm.PeripheralMethodSupplier;
|
||||||
import dan200.computercraft.core.computer.ComputerThread;
|
|
||||||
import dan200.computercraft.core.computer.GlobalEnvironment;
|
import dan200.computercraft.core.computer.GlobalEnvironment;
|
||||||
|
import dan200.computercraft.core.computer.computerthread.ComputerScheduler;
|
||||||
|
import dan200.computercraft.core.computer.computerthread.ComputerThread;
|
||||||
import dan200.computercraft.core.computer.mainthread.MainThreadScheduler;
|
import dan200.computercraft.core.computer.mainthread.MainThreadScheduler;
|
||||||
import dan200.computercraft.core.computer.mainthread.NoWorkMainThreadScheduler;
|
import dan200.computercraft.core.computer.mainthread.NoWorkMainThreadScheduler;
|
||||||
import dan200.computercraft.core.lua.CobaltLuaMachine;
|
import dan200.computercraft.core.lua.CobaltLuaMachine;
|
||||||
@ -31,7 +32,7 @@ import java.util.concurrent.TimeUnit;
|
|||||||
*/
|
*/
|
||||||
public final class ComputerContext {
|
public final class ComputerContext {
|
||||||
private final GlobalEnvironment globalEnvironment;
|
private final GlobalEnvironment globalEnvironment;
|
||||||
private final ComputerThread computerScheduler;
|
private final ComputerScheduler computerScheduler;
|
||||||
private final MainThreadScheduler mainThreadScheduler;
|
private final MainThreadScheduler mainThreadScheduler;
|
||||||
private final ILuaMachine.Factory luaFactory;
|
private final ILuaMachine.Factory luaFactory;
|
||||||
private final List<ILuaAPIFactory> apiFactories;
|
private final List<ILuaAPIFactory> apiFactories;
|
||||||
@ -39,7 +40,7 @@ public final class ComputerContext {
|
|||||||
private final MethodSupplier<PeripheralMethod> peripheralMethods;
|
private final MethodSupplier<PeripheralMethod> peripheralMethods;
|
||||||
|
|
||||||
ComputerContext(
|
ComputerContext(
|
||||||
GlobalEnvironment globalEnvironment, ComputerThread computerScheduler,
|
GlobalEnvironment globalEnvironment, ComputerScheduler computerScheduler,
|
||||||
MainThreadScheduler mainThreadScheduler, ILuaMachine.Factory luaFactory,
|
MainThreadScheduler mainThreadScheduler, ILuaMachine.Factory luaFactory,
|
||||||
List<ILuaAPIFactory> apiFactories, MethodSupplier<LuaMethod> luaMethods,
|
List<ILuaAPIFactory> apiFactories, MethodSupplier<LuaMethod> luaMethods,
|
||||||
MethodSupplier<PeripheralMethod> peripheralMethods
|
MethodSupplier<PeripheralMethod> peripheralMethods
|
||||||
@ -68,7 +69,7 @@ public final class ComputerContext {
|
|||||||
*
|
*
|
||||||
* @return The current computer thread manager.
|
* @return The current computer thread manager.
|
||||||
*/
|
*/
|
||||||
public ComputerThread computerScheduler() {
|
public ComputerScheduler computerScheduler() {
|
||||||
return computerScheduler;
|
return computerScheduler;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,7 +163,7 @@ public final class ComputerContext {
|
|||||||
*/
|
*/
|
||||||
public static class Builder {
|
public static class Builder {
|
||||||
private final GlobalEnvironment environment;
|
private final GlobalEnvironment environment;
|
||||||
private int threads = 1;
|
private @Nullable ComputerScheduler computerScheduler = null;
|
||||||
private @Nullable MainThreadScheduler mainThreadScheduler;
|
private @Nullable MainThreadScheduler mainThreadScheduler;
|
||||||
private @Nullable ILuaMachine.Factory luaFactory;
|
private @Nullable ILuaMachine.Factory luaFactory;
|
||||||
private @Nullable List<ILuaAPIFactory> apiFactories;
|
private @Nullable List<ILuaAPIFactory> apiFactories;
|
||||||
@ -173,7 +174,7 @@ public final class ComputerContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the number of threads the {@link ComputerThread} will use.
|
* Set the {@link #computerScheduler()} to use {@link ComputerThread} with a given number of threads.
|
||||||
*
|
*
|
||||||
* @param threads The number of threads to use.
|
* @param threads The number of threads to use.
|
||||||
* @return {@code this}, for chaining
|
* @return {@code this}, for chaining
|
||||||
@ -181,7 +182,20 @@ public final class ComputerContext {
|
|||||||
*/
|
*/
|
||||||
public Builder computerThreads(int threads) {
|
public Builder computerThreads(int threads) {
|
||||||
if (threads < 1) throw new IllegalArgumentException("Threads must be >= 1");
|
if (threads < 1) throw new IllegalArgumentException("Threads must be >= 1");
|
||||||
this.threads = threads;
|
return computerScheduler(new ComputerThread(threads));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the {@link ComputerScheduler} for this context.
|
||||||
|
*
|
||||||
|
* @param scheduler The computer thread scheduler.
|
||||||
|
* @return {@code this}, for chaining
|
||||||
|
* @see ComputerContext#mainThreadScheduler()
|
||||||
|
*/
|
||||||
|
public Builder computerScheduler(ComputerScheduler scheduler) {
|
||||||
|
Objects.requireNonNull(scheduler);
|
||||||
|
if (computerScheduler != null) throw new IllegalStateException("Computer scheduler already specified");
|
||||||
|
computerScheduler = scheduler;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,7 +264,7 @@ public final class ComputerContext {
|
|||||||
public ComputerContext build() {
|
public ComputerContext build() {
|
||||||
return new ComputerContext(
|
return new ComputerContext(
|
||||||
environment,
|
environment,
|
||||||
new ComputerThread(threads),
|
computerScheduler == null ? new ComputerThread(1) : computerScheduler,
|
||||||
mainThreadScheduler == null ? new NoWorkMainThreadScheduler() : mainThreadScheduler,
|
mainThreadScheduler == null ? new NoWorkMainThreadScheduler() : mainThreadScheduler,
|
||||||
luaFactory == null ? CobaltLuaMachine::new : luaFactory,
|
luaFactory == null ? CobaltLuaMachine::new : luaFactory,
|
||||||
apiFactories == null ? List.of() : apiFactories,
|
apiFactories == null ? List.of() : apiFactories,
|
||||||
|
@ -10,6 +10,8 @@ import dan200.computercraft.api.lua.ILuaAPI;
|
|||||||
import dan200.computercraft.core.ComputerContext;
|
import dan200.computercraft.core.ComputerContext;
|
||||||
import dan200.computercraft.core.CoreConfig;
|
import dan200.computercraft.core.CoreConfig;
|
||||||
import dan200.computercraft.core.apis.*;
|
import dan200.computercraft.core.apis.*;
|
||||||
|
import dan200.computercraft.core.computer.computerthread.ComputerScheduler;
|
||||||
|
import dan200.computercraft.core.computer.computerthread.ComputerThread;
|
||||||
import dan200.computercraft.core.filesystem.FileSystem;
|
import dan200.computercraft.core.filesystem.FileSystem;
|
||||||
import dan200.computercraft.core.filesystem.FileSystemException;
|
import dan200.computercraft.core.filesystem.FileSystemException;
|
||||||
import dan200.computercraft.core.lua.ILuaMachine;
|
import dan200.computercraft.core.lua.ILuaMachine;
|
||||||
@ -17,7 +19,6 @@ import dan200.computercraft.core.lua.MachineEnvironment;
|
|||||||
import dan200.computercraft.core.lua.MachineException;
|
import dan200.computercraft.core.lua.MachineException;
|
||||||
import dan200.computercraft.core.methods.LuaMethod;
|
import dan200.computercraft.core.methods.LuaMethod;
|
||||||
import dan200.computercraft.core.methods.MethodSupplier;
|
import dan200.computercraft.core.methods.MethodSupplier;
|
||||||
import dan200.computercraft.core.metrics.Metrics;
|
|
||||||
import dan200.computercraft.core.metrics.MetricsObserver;
|
import dan200.computercraft.core.metrics.MetricsObserver;
|
||||||
import dan200.computercraft.core.util.Colour;
|
import dan200.computercraft.core.util.Colour;
|
||||||
import dan200.computercraft.core.util.Nullability;
|
import dan200.computercraft.core.util.Nullability;
|
||||||
@ -25,13 +26,13 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
import javax.annotation.concurrent.GuardedBy;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -54,7 +55,7 @@ import java.util.concurrent.locks.ReentrantLock;
|
|||||||
* One final responsibility for the executor is calling {@link ILuaAPI#update()} every tick, via the {@link #tick()}
|
* One final responsibility for the executor is calling {@link ILuaAPI#update()} every tick, via the {@link #tick()}
|
||||||
* method. This should only be called when the computer is actually on ({@link #isOn}).
|
* method. This should only be called when the computer is actually on ({@link #isOn}).
|
||||||
*/
|
*/
|
||||||
final class ComputerExecutor {
|
final class ComputerExecutor implements ComputerScheduler.Worker {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(ComputerExecutor.class);
|
private static final Logger LOG = LoggerFactory.getLogger(ComputerExecutor.class);
|
||||||
private static final int QUEUE_LIMIT = 256;
|
private static final int QUEUE_LIMIT = 256;
|
||||||
|
|
||||||
@ -62,9 +63,7 @@ final class ComputerExecutor {
|
|||||||
private final ComputerEnvironment computerEnvironment;
|
private final ComputerEnvironment computerEnvironment;
|
||||||
private final MetricsObserver metrics;
|
private final MetricsObserver metrics;
|
||||||
private final List<ApiWrapper> apis = new ArrayList<>();
|
private final List<ApiWrapper> apis = new ArrayList<>();
|
||||||
private final ComputerThread scheduler;
|
|
||||||
private final MethodSupplier<LuaMethod> luaMethods;
|
private final MethodSupplier<LuaMethod> luaMethods;
|
||||||
final TimeoutState timeout;
|
|
||||||
|
|
||||||
private @Nullable FileSystem fileSystem;
|
private @Nullable FileSystem fileSystem;
|
||||||
|
|
||||||
@ -92,34 +91,11 @@ final class ComputerExecutor {
|
|||||||
private final ReentrantLock isOnLock = new ReentrantLock();
|
private final ReentrantLock isOnLock = new ReentrantLock();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A lock used for any changes to {@link #eventQueue}, {@link #command} or {@link #onComputerQueue}. This will be
|
* A lock used for any changes to {@link #eventQueue} or {@link #command}. This will be used on the main thread,
|
||||||
* used on the main thread, so locks should be kept as brief as possible.
|
* so locks should be kept as brief as possible.
|
||||||
*/
|
*/
|
||||||
private final Object queueLock = new Object();
|
private final Object queueLock = new Object();
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if this executor is present within {@link ComputerThread}.
|
|
||||||
*
|
|
||||||
* @see #queueLock
|
|
||||||
* @see #enqueue()
|
|
||||||
* @see #afterWork()
|
|
||||||
*/
|
|
||||||
volatile boolean onComputerQueue = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The amount of time this computer has used on a theoretical machine which shares work evenly amongst computers.
|
|
||||||
*
|
|
||||||
* @see ComputerThread
|
|
||||||
*/
|
|
||||||
long virtualRuntime = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The last time at which we updated {@link #virtualRuntime}.
|
|
||||||
*
|
|
||||||
* @see ComputerThread
|
|
||||||
*/
|
|
||||||
long vRuntimeStart;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The command that {@link #work()} should execute on the computer thread.
|
* The command that {@link #work()} should execute on the computer thread.
|
||||||
* <p>
|
* <p>
|
||||||
@ -129,6 +105,7 @@ final class ComputerExecutor {
|
|||||||
* Note, if command is not {@code null}, then some command is scheduled to be executed. Otherwise it is not
|
* Note, if command is not {@code null}, then some command is scheduled to be executed. Otherwise it is not
|
||||||
* currently in the queue (or is currently being executed).
|
* currently in the queue (or is currently being executed).
|
||||||
*/
|
*/
|
||||||
|
@GuardedBy("queueLock")
|
||||||
private volatile @Nullable StateCommand command;
|
private volatile @Nullable StateCommand command;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -136,43 +113,45 @@ final class ComputerExecutor {
|
|||||||
* <p>
|
* <p>
|
||||||
* Note, this should be empty if this computer is off - it is cleared on shutdown and when turning on again.
|
* Note, this should be empty if this computer is off - it is cleared on shutdown and when turning on again.
|
||||||
*/
|
*/
|
||||||
|
@GuardedBy("queueLock")
|
||||||
private final Queue<Event> eventQueue = new ArrayDeque<>(4);
|
private final Queue<Event> eventQueue = new ArrayDeque<>(4);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether we interrupted an event and so should resume it instead of executing another task.
|
* Whether this computer was paused (and so should resume without pulling an event) or not.
|
||||||
*
|
*
|
||||||
|
* @see #timeRemaining
|
||||||
* @see #work()
|
* @see #work()
|
||||||
* @see #resumeMachine(String, Object[])
|
* @see #resumeMachine(String, Object[])
|
||||||
*/
|
*/
|
||||||
private boolean interruptedEvent = false;
|
private boolean wasPaused;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The amount of time this computer can run for before being interrupted. This is only defined when
|
||||||
|
* {@link #wasPaused} is set.
|
||||||
|
*/
|
||||||
|
private long timeRemaining = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether this executor has been closed, and will no longer accept any incoming commands or events.
|
* Whether this executor has been closed, and will no longer accept any incoming commands or events.
|
||||||
*
|
*
|
||||||
* @see #queueStop(boolean, boolean)
|
* @see #queueStop(boolean, boolean)
|
||||||
*/
|
*/
|
||||||
|
@GuardedBy("queueLock")
|
||||||
private boolean closed;
|
private boolean closed;
|
||||||
|
|
||||||
private @Nullable WritableMount rootMount;
|
private @Nullable WritableMount rootMount;
|
||||||
|
|
||||||
/**
|
|
||||||
* The thread the executor is running on. This is non-null when performing work. We use this to ensure we're only
|
|
||||||
* doing one bit of work at one time.
|
|
||||||
*
|
|
||||||
* @see ComputerThread
|
|
||||||
*/
|
|
||||||
final AtomicReference<Thread> executingThread = new AtomicReference<>();
|
|
||||||
|
|
||||||
private final ILuaMachine.Factory luaFactory;
|
private final ILuaMachine.Factory luaFactory;
|
||||||
|
|
||||||
|
private final ComputerScheduler.Executor executor;
|
||||||
|
|
||||||
ComputerExecutor(Computer computer, ComputerEnvironment computerEnvironment, ComputerContext context) {
|
ComputerExecutor(Computer computer, ComputerEnvironment computerEnvironment, ComputerContext context) {
|
||||||
this.computer = computer;
|
this.computer = computer;
|
||||||
this.computerEnvironment = computerEnvironment;
|
this.computerEnvironment = computerEnvironment;
|
||||||
metrics = computerEnvironment.getMetrics();
|
metrics = computerEnvironment.getMetrics();
|
||||||
luaFactory = context.luaFactory();
|
luaFactory = context.luaFactory();
|
||||||
scheduler = context.computerScheduler();
|
|
||||||
luaMethods = context.luaMethods();
|
luaMethods = context.luaMethods();
|
||||||
timeout = new TimeoutState(scheduler);
|
executor = context.computerScheduler().createExecutor(this, metrics);
|
||||||
|
|
||||||
var environment = computer.getEnvironment();
|
var environment = computer.getEnvironment();
|
||||||
|
|
||||||
@ -192,6 +171,11 @@ final class ComputerExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getComputerID() {
|
||||||
|
return computer.getID();
|
||||||
|
}
|
||||||
|
|
||||||
boolean isOn() {
|
boolean isOn() {
|
||||||
return isOn;
|
return isOn;
|
||||||
}
|
}
|
||||||
@ -202,10 +186,6 @@ final class ComputerExecutor {
|
|||||||
return fileSystem;
|
return fileSystem;
|
||||||
}
|
}
|
||||||
|
|
||||||
Computer getComputer() {
|
|
||||||
return computer;
|
|
||||||
}
|
|
||||||
|
|
||||||
void addApi(ILuaAPI api) {
|
void addApi(ILuaAPI api) {
|
||||||
apis.add(new ApiWrapper(api, null));
|
apis.add(new ApiWrapper(api, null));
|
||||||
}
|
}
|
||||||
@ -219,8 +199,8 @@ final class ComputerExecutor {
|
|||||||
if (closed || isOn || command != null) return;
|
if (closed || isOn || command != null) return;
|
||||||
|
|
||||||
command = StateCommand.TURN_ON;
|
command = StateCommand.TURN_ON;
|
||||||
enqueue();
|
|
||||||
}
|
}
|
||||||
|
enqueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -245,24 +225,31 @@ final class ComputerExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
command = newCommand;
|
command = newCommand;
|
||||||
enqueue();
|
|
||||||
}
|
}
|
||||||
|
enqueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abort this whole computer due to a timeout. This will immediately destroy the Lua machine,
|
* Abort this whole computer due to a timeout. This will immediately destroy the Lua machine,
|
||||||
* and then schedule a shutdown.
|
* and then schedule a shutdown.
|
||||||
*/
|
*/
|
||||||
void abort() {
|
@Override
|
||||||
immediateFail(StateCommand.ABORT);
|
public void abortWithTimeout() {
|
||||||
|
immediateFail(StateCommand.ABORT_WITH_TIMEOUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abort this whole computer due to an internal error. This will immediately destroy the Lua machine,
|
* Abort this whole computer due to an internal error. This will immediately destroy the Lua machine,
|
||||||
* and then schedule a shutdown.
|
* and then schedule a shutdown.
|
||||||
*/
|
*/
|
||||||
void fastFail() {
|
@Override
|
||||||
immediateFail(StateCommand.ERROR);
|
public void abortWithError() {
|
||||||
|
immediateFail(StateCommand.ABORT_WITH_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unload() {
|
||||||
|
queueStop(false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void immediateFail(StateCommand command) {
|
private void immediateFail(StateCommand command) {
|
||||||
@ -293,17 +280,15 @@ final class ComputerExecutor {
|
|||||||
if (closed || command != null || eventQueue.size() >= QUEUE_LIMIT) return;
|
if (closed || command != null || eventQueue.size() >= QUEUE_LIMIT) return;
|
||||||
|
|
||||||
eventQueue.offer(new Event(event, args));
|
eventQueue.offer(new Event(event, args));
|
||||||
enqueue();
|
|
||||||
}
|
}
|
||||||
|
enqueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add this executor to the {@link ComputerThread} if not already there.
|
* Add this executor to the {@link ComputerThread} if not already there.
|
||||||
*/
|
*/
|
||||||
private void enqueue() {
|
private void enqueue() {
|
||||||
synchronized (queueLock) {
|
executor.submit();
|
||||||
if (!onComputerQueue) scheduler.queue(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -384,7 +369,7 @@ final class ComputerExecutor {
|
|||||||
// Create the lua machine
|
// Create the lua machine
|
||||||
try (var bios = biosStream) {
|
try (var bios = biosStream) {
|
||||||
return luaFactory.create(new MachineEnvironment(
|
return luaFactory.create(new MachineEnvironment(
|
||||||
new LuaContext(computer), metrics, timeout,
|
new LuaContext(computer), metrics, executor.timeoutState(),
|
||||||
() -> apis.stream().map(ApiWrapper::api).iterator(),
|
() -> apis.stream().map(ApiWrapper::api).iterator(),
|
||||||
luaMethods,
|
luaMethods,
|
||||||
computer.getGlobalEnvironment().getHostString()
|
computer.getGlobalEnvironment().getHostString()
|
||||||
@ -404,7 +389,6 @@ final class ComputerExecutor {
|
|||||||
try {
|
try {
|
||||||
// Reset the terminal and event queue
|
// Reset the terminal and event queue
|
||||||
computer.getTerminal().reset();
|
computer.getTerminal().reset();
|
||||||
interruptedEvent = false;
|
|
||||||
synchronized (queueLock) {
|
synchronized (queueLock) {
|
||||||
eventQueue.clear();
|
eventQueue.clear();
|
||||||
}
|
}
|
||||||
@ -432,15 +416,15 @@ final class ComputerExecutor {
|
|||||||
isOnLock.unlock();
|
isOnLock.unlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now actually start the computer, now that everything is set up.
|
// Mark the Lua VM as ready to be executed next time.
|
||||||
resumeMachine(null, null);
|
wasPaused = true;
|
||||||
|
timeRemaining = TimeoutState.TIMEOUT;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void shutdown() throws InterruptedException {
|
private void shutdown() throws InterruptedException {
|
||||||
isOnLock.lockInterruptibly();
|
isOnLock.lockInterruptibly();
|
||||||
try {
|
try {
|
||||||
isOn = false;
|
isOn = wasPaused = false;
|
||||||
interruptedEvent = false;
|
|
||||||
synchronized (queueLock) {
|
synchronized (queueLock) {
|
||||||
eventQueue.clear();
|
eventQueue.clear();
|
||||||
}
|
}
|
||||||
@ -468,36 +452,6 @@ final class ComputerExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Called before calling {@link #work()}, setting up any important state.
|
|
||||||
*/
|
|
||||||
void beforeWork() {
|
|
||||||
vRuntimeStart = System.nanoTime();
|
|
||||||
timeout.startTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called after executing {@link #work()}.
|
|
||||||
*
|
|
||||||
* @return If we have more work to do.
|
|
||||||
*/
|
|
||||||
boolean afterWork() {
|
|
||||||
if (interruptedEvent) {
|
|
||||||
timeout.pauseTimer();
|
|
||||||
} else {
|
|
||||||
timeout.stopTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
metrics.observe(Metrics.COMPUTER_TASKS, timeout.nanoCurrent());
|
|
||||||
|
|
||||||
if (interruptedEvent) return true;
|
|
||||||
|
|
||||||
synchronized (queueLock) {
|
|
||||||
if (eventQueue.isEmpty() && command == null) return onComputerQueue = false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main worker function, called by {@link ComputerThread}.
|
* The main worker function, called by {@link ComputerThread}.
|
||||||
* <p>
|
* <p>
|
||||||
@ -507,15 +461,15 @@ final class ComputerExecutor {
|
|||||||
* @see #command
|
* @see #command
|
||||||
* @see #eventQueue
|
* @see #eventQueue
|
||||||
*/
|
*/
|
||||||
void work() throws InterruptedException {
|
@Override
|
||||||
if (interruptedEvent && !closed) {
|
public void work() throws InterruptedException {
|
||||||
interruptedEvent = false;
|
workImpl();
|
||||||
if (machine != null) {
|
synchronized (queueLock) {
|
||||||
resumeMachine(null, null);
|
if (wasPaused || command != null || !eventQueue.isEmpty()) enqueue();
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void workImpl() throws InterruptedException {
|
||||||
StateCommand command;
|
StateCommand command;
|
||||||
Event event = null;
|
Event event = null;
|
||||||
synchronized (queueLock) {
|
synchronized (queueLock) {
|
||||||
@ -523,7 +477,7 @@ final class ComputerExecutor {
|
|||||||
this.command = null;
|
this.command = null;
|
||||||
|
|
||||||
// If we've no command, pull something from the event queue instead.
|
// If we've no command, pull something from the event queue instead.
|
||||||
if (command == null) {
|
if (command == null && !wasPaused) {
|
||||||
if (!isOn) {
|
if (!isOn) {
|
||||||
// We're not on and had no command, but we had work queued. This should never happen, so clear
|
// We're not on and had no command, but we had work queued. This should never happen, so clear
|
||||||
// the event queue just in case.
|
// the event queue just in case.
|
||||||
@ -536,6 +490,7 @@ final class ComputerExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (command != null) {
|
if (command != null) {
|
||||||
|
wasPaused = false;
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case TURN_ON -> {
|
case TURN_ON -> {
|
||||||
if (isOn) return;
|
if (isOn) return;
|
||||||
@ -552,23 +507,29 @@ final class ComputerExecutor {
|
|||||||
shutdown();
|
shutdown();
|
||||||
computer.turnOn();
|
computer.turnOn();
|
||||||
}
|
}
|
||||||
case ABORT -> {
|
case ABORT_WITH_TIMEOUT -> {
|
||||||
if (!isOn) return;
|
if (!isOn) return;
|
||||||
displayFailure("Error running computer", TimeoutState.ABORT_MESSAGE);
|
displayFailure("Error running computer", TimeoutState.ABORT_MESSAGE);
|
||||||
shutdown();
|
shutdown();
|
||||||
}
|
}
|
||||||
case ERROR -> {
|
case ABORT_WITH_ERROR -> {
|
||||||
if (!isOn) return;
|
if (!isOn) return;
|
||||||
displayFailure("Error running computer", "An internal error occurred, see logs.");
|
displayFailure("Error running computer", "An internal error occurred, see logs.");
|
||||||
shutdown();
|
shutdown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (wasPaused) {
|
||||||
|
executor.setRemainingTime(timeRemaining);
|
||||||
|
resumeMachine(null, null);
|
||||||
} else if (event != null) {
|
} else if (event != null) {
|
||||||
|
executor.setRemainingTime(TimeoutState.TIMEOUT);
|
||||||
resumeMachine(event.name, event.args);
|
resumeMachine(event.name, event.args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void printState(StringBuilder out) {
|
@Override
|
||||||
|
@SuppressWarnings("GuardedBy")
|
||||||
|
public void writeState(StringBuilder out) {
|
||||||
out.append("Enqueued command: ").append(command).append('\n');
|
out.append("Enqueued command: ").append(command).append('\n');
|
||||||
out.append("Enqueued events: ").append(eventQueue.size()).append('\n');
|
out.append("Enqueued events: ").append(eventQueue.size()).append('\n');
|
||||||
|
|
||||||
@ -599,19 +560,23 @@ final class ComputerExecutor {
|
|||||||
|
|
||||||
private void resumeMachine(@Nullable String event, @Nullable Object[] args) throws InterruptedException {
|
private void resumeMachine(@Nullable String event, @Nullable Object[] args) throws InterruptedException {
|
||||||
var result = Nullability.assertNonNull(machine).handleEvent(event, args);
|
var result = Nullability.assertNonNull(machine).handleEvent(event, args);
|
||||||
interruptedEvent = result.isPause();
|
if (result.isError()) {
|
||||||
if (!result.isError()) return;
|
displayFailure("Error running computer", result.getMessage());
|
||||||
|
shutdown();
|
||||||
displayFailure("Error running computer", result.getMessage());
|
} else if (result.isPause()) {
|
||||||
shutdown();
|
wasPaused = true;
|
||||||
|
timeRemaining = executor.getRemainingTime();
|
||||||
|
} else {
|
||||||
|
wasPaused = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum StateCommand {
|
private enum StateCommand {
|
||||||
TURN_ON,
|
TURN_ON,
|
||||||
SHUTDOWN,
|
SHUTDOWN,
|
||||||
REBOOT,
|
REBOOT,
|
||||||
ABORT,
|
ABORT_WITH_TIMEOUT,
|
||||||
ERROR,
|
ABORT_WITH_ERROR,
|
||||||
}
|
}
|
||||||
|
|
||||||
private record Event(String name, @Nullable Object[] args) {
|
private record Event(String name, @Nullable Object[] args) {
|
||||||
|
@ -5,6 +5,8 @@
|
|||||||
package dan200.computercraft.core.computer;
|
package dan200.computercraft.core.computer;
|
||||||
|
|
||||||
import com.google.errorprone.annotations.concurrent.GuardedBy;
|
import com.google.errorprone.annotations.concurrent.GuardedBy;
|
||||||
|
import dan200.computercraft.core.computer.computerthread.ComputerScheduler;
|
||||||
|
import dan200.computercraft.core.computer.computerthread.ManagedTimeoutState;
|
||||||
import dan200.computercraft.core.lua.ILuaMachine;
|
import dan200.computercraft.core.lua.ILuaMachine;
|
||||||
import dan200.computercraft.core.lua.MachineResult;
|
import dan200.computercraft.core.lua.MachineResult;
|
||||||
|
|
||||||
@ -24,90 +26,54 @@ import java.util.concurrent.TimeUnit;
|
|||||||
* (namely, throwing a "Too long without yielding" error).
|
* (namely, throwing a "Too long without yielding" error).
|
||||||
* <p>
|
* <p>
|
||||||
* Now, if a computer still does not stop after that period, they're behaving really badly. 1.5 seconds after a soft
|
* Now, if a computer still does not stop after that period, they're behaving really badly. 1.5 seconds after a soft
|
||||||
* abort ({@link #ABORT_TIMEOUT}), we trigger a hard abort (note, this is done from the computer thread manager). This
|
* abort ({@link #ABORT_TIMEOUT}), we trigger a hard abort. This will destroy the entire Lua runtime and shut the
|
||||||
* will destroy the entire Lua runtime and shut the computer down.
|
* computer down.
|
||||||
* <p>
|
* <p>
|
||||||
* The Lua runtime is also allowed to pause execution if there are other computers contesting for work. All computers
|
* The Lua runtime is also allowed to pause execution if there are other computers contesting for work. All computers
|
||||||
* are allowed to run for {@link ComputerThread#scaledPeriod()} nanoseconds (see {@link #currentDeadline}). After that
|
* are guaranteed to run for some time. After that period, if any computers are waiting to be executed then we'll set
|
||||||
* period, if any computers are waiting to be executed then we'll set the paused flag to true ({@link #isPaused()}.
|
* the paused flag to true ({@link #isPaused()}.
|
||||||
*
|
*
|
||||||
* @see ComputerThread
|
* @see ComputerScheduler
|
||||||
|
* @see ManagedTimeoutState
|
||||||
* @see ILuaMachine
|
* @see ILuaMachine
|
||||||
* @see MachineResult#isPause()
|
* @see MachineResult#isPause()
|
||||||
*/
|
*/
|
||||||
public final class TimeoutState {
|
public abstract class TimeoutState {
|
||||||
/**
|
/**
|
||||||
* The total time a task is allowed to run before aborting in nanoseconds.
|
* The time (in nanoseconds) are computer is allowed to run for its long-running tasks, such as startup and
|
||||||
|
* shutdown.
|
||||||
*/
|
*/
|
||||||
static final long TIMEOUT = TimeUnit.MILLISECONDS.toNanos(7000);
|
public static final long BASE_TIMEOUT = TimeUnit.SECONDS.toNanos(30);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total time the Lua VM is allowed to run before aborting in nanoseconds.
|
||||||
|
*/
|
||||||
|
public static final long TIMEOUT = TimeUnit.MILLISECONDS.toNanos(7000);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The time the task is allowed to run after each abort in nanoseconds.
|
* The time the task is allowed to run after each abort in nanoseconds.
|
||||||
*/
|
*/
|
||||||
static final long ABORT_TIMEOUT = TimeUnit.MILLISECONDS.toNanos(1500);
|
public static final long ABORT_TIMEOUT = TimeUnit.MILLISECONDS.toNanos(1500);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The error message to display when we trigger an abort.
|
* The error message to display when we trigger an abort.
|
||||||
*/
|
*/
|
||||||
public static final String ABORT_MESSAGE = "Too long without yielding";
|
public static final String ABORT_MESSAGE = "Too long without yielding";
|
||||||
|
|
||||||
private final ComputerThread scheduler;
|
|
||||||
@GuardedBy("this")
|
@GuardedBy("this")
|
||||||
private final List<Runnable> listeners = new ArrayList<>(0);
|
private final List<Runnable> listeners = new ArrayList<>(0);
|
||||||
|
|
||||||
private boolean paused;
|
protected boolean paused;
|
||||||
private boolean softAbort;
|
protected boolean softAbort;
|
||||||
private volatile boolean hardAbort;
|
protected volatile boolean hardAbort;
|
||||||
|
|
||||||
/**
|
|
||||||
* When the cumulative time would have started had the whole event been processed in one go.
|
|
||||||
*/
|
|
||||||
private long cumulativeStart;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* How much cumulative time has elapsed. This is effectively {@code cumulativeStart - currentStart}.
|
|
||||||
*/
|
|
||||||
private long cumulativeElapsed;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When this execution round started.
|
|
||||||
*/
|
|
||||||
private long currentStart;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When this execution round should look potentially be paused.
|
|
||||||
*/
|
|
||||||
private long currentDeadline;
|
|
||||||
|
|
||||||
public TimeoutState(ComputerThread scheduler) {
|
|
||||||
this.scheduler = scheduler;
|
|
||||||
}
|
|
||||||
|
|
||||||
long nanoCumulative() {
|
|
||||||
return System.nanoTime() - cumulativeStart;
|
|
||||||
}
|
|
||||||
|
|
||||||
long nanoCurrent() {
|
|
||||||
return System.nanoTime() - currentStart;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recompute the {@link #isSoftAborted()} and {@link #isPaused()} flags.
|
* Recompute the {@link #isSoftAborted()} and {@link #isPaused()} flags.
|
||||||
|
* <p>
|
||||||
|
* Normally this will be called automatically by the {@link ComputerScheduler}, but it may be useful to call this
|
||||||
|
* manually if the most up-to-date information is needed.
|
||||||
*/
|
*/
|
||||||
public synchronized void refresh() {
|
public abstract void refresh();
|
||||||
// Important: The weird arithmetic here is important, as nanoTime may return negative values, and so we
|
|
||||||
// need to handle overflow.
|
|
||||||
var now = System.nanoTime();
|
|
||||||
var changed = false;
|
|
||||||
if (!paused && (paused = currentDeadline - now <= 0 && scheduler.hasPendingWork())) { // now >= currentDeadline
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
if (!softAbort && (softAbort = now - cumulativeStart - TIMEOUT >= 0)) { // now - cumulativeStart >= TIMEOUT
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changed) updateListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether we should pause execution of this machine.
|
* Whether we should pause execution of this machine.
|
||||||
@ -117,7 +83,7 @@ public final class TimeoutState {
|
|||||||
*
|
*
|
||||||
* @return Whether we should pause execution.
|
* @return Whether we should pause execution.
|
||||||
*/
|
*/
|
||||||
public boolean isPaused() {
|
public final boolean isPaused() {
|
||||||
return paused;
|
return paused;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,7 +92,7 @@ public final class TimeoutState {
|
|||||||
*
|
*
|
||||||
* @return {@code true} if we should throw a timeout error.
|
* @return {@code true} if we should throw a timeout error.
|
||||||
*/
|
*/
|
||||||
public boolean isSoftAborted() {
|
public final boolean isSoftAborted() {
|
||||||
return softAbort;
|
return softAbort;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,64 +101,22 @@ public final class TimeoutState {
|
|||||||
*
|
*
|
||||||
* @return {@code true} if the machine should be forcibly shut down.
|
* @return {@code true} if the machine should be forcibly shut down.
|
||||||
*/
|
*/
|
||||||
public boolean isHardAborted() {
|
public final boolean isHardAborted() {
|
||||||
return hardAbort;
|
return hardAbort;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* If the machine should be forcibly aborted.
|
|
||||||
*/
|
|
||||||
void hardAbort() {
|
|
||||||
softAbort = hardAbort = true;
|
|
||||||
synchronized (this) {
|
|
||||||
updateListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start the current and cumulative timers again.
|
|
||||||
*/
|
|
||||||
void startTimer() {
|
|
||||||
var now = System.nanoTime();
|
|
||||||
currentStart = now;
|
|
||||||
currentDeadline = now + scheduler.scaledPeriod();
|
|
||||||
// Compute the "nominal start time".
|
|
||||||
cumulativeStart = now - cumulativeElapsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pauses the cumulative time, to be resumed by {@link #startTimer()}.
|
|
||||||
*
|
|
||||||
* @see #nanoCumulative()
|
|
||||||
*/
|
|
||||||
synchronized void pauseTimer() {
|
|
||||||
// We set the cumulative time to difference between current time and "nominal start time".
|
|
||||||
cumulativeElapsed = System.nanoTime() - cumulativeStart;
|
|
||||||
paused = false;
|
|
||||||
updateListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets the cumulative time and resets the abort flags.
|
|
||||||
*/
|
|
||||||
synchronized void stopTimer() {
|
|
||||||
cumulativeElapsed = 0;
|
|
||||||
paused = softAbort = hardAbort = false;
|
|
||||||
updateListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
@GuardedBy("this")
|
@GuardedBy("this")
|
||||||
private void updateListeners() {
|
protected final void updateListeners() {
|
||||||
for (var listener : listeners) listener.run();
|
for (var listener : listeners) listener.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void addListener(Runnable listener) {
|
public final synchronized void addListener(Runnable listener) {
|
||||||
Objects.requireNonNull(listener, "listener cannot be null");
|
Objects.requireNonNull(listener, "listener cannot be null");
|
||||||
listeners.add(listener);
|
listeners.add(listener);
|
||||||
listener.run();
|
listener.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void removeListener(Runnable listener) {
|
public final synchronized void removeListener(Runnable listener) {
|
||||||
Objects.requireNonNull(listener, "listener cannot be null");
|
Objects.requireNonNull(listener, "listener cannot be null");
|
||||||
listeners.remove(listener);
|
listeners.remove(listener);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,127 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.core.computer.computerthread;
|
||||||
|
|
||||||
|
import dan200.computercraft.core.computer.TimeoutState;
|
||||||
|
import dan200.computercraft.core.metrics.MetricsObserver;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link ComputerScheduler} is responsible for executing computers on the computer thread(s).
|
||||||
|
* <p>
|
||||||
|
* This handles both scheduling the computers for work across multiple threads, as well as {@linkplain TimeoutState timing out}
|
||||||
|
* or pausing the computer if they execute for too long.
|
||||||
|
* <p>
|
||||||
|
* This API is composed of two interfaces, a {@link Worker} and {@link Executor}. The {@link ComputerScheduler}
|
||||||
|
* implementation will supply an {@link Executor}, while consuming classes should implement {@link Worker}.
|
||||||
|
* <p>
|
||||||
|
* In practice, this interface is only implemented by {@link ComputerThread} (and consumed by {@link dan200.computercraft.core.computer.ComputerExecutor}),
|
||||||
|
* however this interface is useful to enforce separation of the two.
|
||||||
|
*
|
||||||
|
* @see ManagedTimeoutState
|
||||||
|
*/
|
||||||
|
public interface ComputerScheduler {
|
||||||
|
Executor createExecutor(Worker worker, MetricsObserver metrics);
|
||||||
|
|
||||||
|
boolean stop(long timeout, TimeUnit unit) throws InterruptedException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link Executor} holds the state of a {@link Worker} within the scheduler.
|
||||||
|
* <p>
|
||||||
|
* This is used to schedule the worker for execution, as well as providing some additional control over the
|
||||||
|
* {@link TimeoutState}.
|
||||||
|
*/
|
||||||
|
interface Executor {
|
||||||
|
/**
|
||||||
|
* Submit the executor to the scheduler, marking it as ready {@linkplain Worker#work() to run some work}.
|
||||||
|
* <p>
|
||||||
|
* This function is idempotent - if the executor is already queued, nothing will happen.
|
||||||
|
*/
|
||||||
|
void submit();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the executor's {@link TimeoutState}.
|
||||||
|
*
|
||||||
|
* @return The executor's timeout state.
|
||||||
|
*/
|
||||||
|
TimeoutState timeoutState();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the amount of time this computer can run for before being interrupted.
|
||||||
|
* <p>
|
||||||
|
* This value starts off as {@link TimeoutState#BASE_TIMEOUT}, but may be reduced by
|
||||||
|
* {@link #setRemainingTime(long)}.
|
||||||
|
*
|
||||||
|
* @return The time this computer can run for being interrupted.
|
||||||
|
* @see #getRemainingTime()
|
||||||
|
*/
|
||||||
|
long getRemainingTime();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the amount of this computer can execute for before being interrupted.
|
||||||
|
* <p>
|
||||||
|
* This value will typically be {@link TimeoutState#TIMEOUT}, but may be a previous value of
|
||||||
|
* {@link #getRemainingTime()} if the computer is resuming after {@linkplain TimeoutState#isPaused() being
|
||||||
|
* paused}.
|
||||||
|
*
|
||||||
|
* @param time The time this computer can execute for.
|
||||||
|
* @see #getRemainingTime()
|
||||||
|
*/
|
||||||
|
void setRemainingTime(long time);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link Worker} is responsible for actually running the computer's code.
|
||||||
|
* <p>
|
||||||
|
* his handles {@linkplain Worker#work() running the actual computer logic}, as well as providing some additional
|
||||||
|
* control methods.
|
||||||
|
* <p>
|
||||||
|
* This should be implemented by the consuming class.
|
||||||
|
*/
|
||||||
|
interface Worker {
|
||||||
|
/**
|
||||||
|
* Perform any work that the computer needs to do, for instance turning on, shutting down or actually running
|
||||||
|
* code.
|
||||||
|
* <p>
|
||||||
|
* If the computer needs to run immediately again, it should call {@link Executor#submit()} within this method.
|
||||||
|
*
|
||||||
|
* @throws InterruptedException If the computer has run for too long and must be terminated.
|
||||||
|
*/
|
||||||
|
void work() throws InterruptedException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the ID of this computer, used in error messages.
|
||||||
|
*
|
||||||
|
* @return This computers ID.
|
||||||
|
*/
|
||||||
|
int getComputerID();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write any useful debugging information computer to the provided buffer. This is used in log messages when the
|
||||||
|
* computer has run for too long.
|
||||||
|
*
|
||||||
|
* @param output The buffer to write to.
|
||||||
|
*/
|
||||||
|
void writeState(StringBuilder output);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abort this whole computer due to a timeout.
|
||||||
|
*/
|
||||||
|
void abortWithTimeout();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abort this whole computer due to some internal error.
|
||||||
|
*/
|
||||||
|
void abortWithError();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Unload" this computer, shutting it down and preventing it from running again.
|
||||||
|
* <p>
|
||||||
|
* This is called by the scheduler when {@linkplain ComputerScheduler#stop(long, TimeUnit) it is stopped.}
|
||||||
|
*/
|
||||||
|
void unload();
|
||||||
|
}
|
||||||
|
}
|
@ -2,26 +2,26 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package dan200.computercraft.core.computer;
|
package dan200.computercraft.core.computer.computerthread;
|
||||||
|
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import com.google.errorprone.annotations.concurrent.GuardedBy;
|
|
||||||
import dan200.computercraft.core.ComputerContext;
|
import dan200.computercraft.core.ComputerContext;
|
||||||
import dan200.computercraft.core.Logging;
|
import dan200.computercraft.core.Logging;
|
||||||
|
import dan200.computercraft.core.computer.TimeoutState;
|
||||||
|
import dan200.computercraft.core.metrics.Metrics;
|
||||||
|
import dan200.computercraft.core.metrics.MetricsObserver;
|
||||||
import dan200.computercraft.core.util.ThreadUtils;
|
import dan200.computercraft.core.util.ThreadUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
import javax.annotation.concurrent.GuardedBy;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
import java.util.concurrent.ThreadFactory;
|
import java.util.concurrent.ThreadFactory;
|
||||||
import java.util.concurrent.ThreadPoolExecutor;
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.*;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
import java.util.concurrent.locks.Condition;
|
import java.util.concurrent.locks.Condition;
|
||||||
import java.util.concurrent.locks.LockSupport;
|
import java.util.concurrent.locks.LockSupport;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
@ -29,9 +29,9 @@ import java.util.concurrent.locks.ReentrantLock;
|
|||||||
/**
|
/**
|
||||||
* Runs all scheduled tasks for computers in a {@link ComputerContext}.
|
* Runs all scheduled tasks for computers in a {@link ComputerContext}.
|
||||||
* <p>
|
* <p>
|
||||||
* This acts as an over-complicated {@link ThreadPoolExecutor}: It creates several {@link Worker} threads which pull
|
* This acts as an over-complicated {@link ThreadPoolExecutor}: It creates several {@linkplain WorkerThread worker
|
||||||
* tasks from a shared queue, executing them. It also creates a single {@link Monitor} thread, which updates computer
|
* threads} which pull tasks from a shared queue, executing them. It also creates a single {@link Monitor} thread, which
|
||||||
* timeouts, killing workers if they have not been terminated by {@link TimeoutState#isSoftAborted()}.
|
* updates computer timeouts, killing workers if they have not been terminated by {@link TimeoutState#isSoftAborted()}.
|
||||||
* <p>
|
* <p>
|
||||||
* Computers are executed using a priority system, with those who have spent less time executing having a higher
|
* Computers are executed using a priority system, with those who have spent less time executing having a higher
|
||||||
* priority than those hogging the thread. This, combined with {@link TimeoutState#isPaused()} means we can reduce the
|
* priority than those hogging the thread. This, combined with {@link TimeoutState#isPaused()} means we can reduce the
|
||||||
@ -39,20 +39,16 @@ import java.util.concurrent.locks.ReentrantLock;
|
|||||||
* <p>
|
* <p>
|
||||||
* This is done using an implementation of Linux's Completely Fair Scheduler. When a computer executes, we compute what
|
* This is done using an implementation of Linux's Completely Fair Scheduler. When a computer executes, we compute what
|
||||||
* share of execution time it has used (time executed/number of tasks). We then pick the computer who has the least
|
* share of execution time it has used (time executed/number of tasks). We then pick the computer who has the least
|
||||||
* "virtual execution time" (aka {@link ComputerExecutor#virtualRuntime}).
|
* "virtual execution time" (aka {@link ExecutorImpl#virtualRuntime}).
|
||||||
* <p>
|
* <p>
|
||||||
* When adding a computer to the queue, we make sure its "virtual runtime" is at least as big as the smallest runtime.
|
* When adding a computer to the queue, we make sure its "virtual runtime" is at least as big as the smallest runtime.
|
||||||
* This means that adding computers which have slept a lot do not then have massive priority over everyone else. See
|
* This means that adding computers which have slept a lot do not then have massive priority over everyone else. See
|
||||||
* {@link #queue(ComputerExecutor)} for how this is implemented.
|
* {@link #queue(ExecutorImpl)} for how this is implemented.
|
||||||
* <p>
|
* <p>
|
||||||
* In reality, it's unlikely that more than a few computers are waiting to execute at once, so this will not have much
|
* In reality, it's unlikely that more than a few computers are waiting to execute at once, so this will not have much
|
||||||
* effect unless you have a computer hogging execution time. However, it is pretty effective in those situations.
|
* effect unless you have a computer hogging execution time. However, it is pretty effective in those situations.
|
||||||
*
|
|
||||||
* @see TimeoutState For how hard timeouts are handled.
|
|
||||||
* @see ComputerExecutor For how computers actually do execution.
|
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("GuardedBy") // FIXME: Hard to know what the correct thing to do is.
|
public final class ComputerThread implements ComputerScheduler {
|
||||||
public final class ComputerThread {
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(ComputerThread.class);
|
private static final Logger LOG = LoggerFactory.getLogger(ComputerThread.class);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -96,7 +92,7 @@ public final class ComputerThread {
|
|||||||
/**
|
/**
|
||||||
* Time difference between reporting crashed threads.
|
* Time difference between reporting crashed threads.
|
||||||
*
|
*
|
||||||
* @see Worker#reportTimeout(ComputerExecutor, long)
|
* @see WorkerThread#reportTimeout(ExecutorImpl, long)
|
||||||
*/
|
*/
|
||||||
private static final long REPORT_DEBOUNCE = TimeUnit.SECONDS.toNanos(1);
|
private static final long REPORT_DEBOUNCE = TimeUnit.SECONDS.toNanos(1);
|
||||||
|
|
||||||
@ -123,7 +119,7 @@ public final class ComputerThread {
|
|||||||
* The array of current workers, and their owning threads.
|
* The array of current workers, and their owning threads.
|
||||||
*/
|
*/
|
||||||
@GuardedBy("threadLock")
|
@GuardedBy("threadLock")
|
||||||
private final Worker[] workers;
|
private final WorkerThread[] workers;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The number of workers in {@link #workers}.
|
* The number of workers in {@link #workers}.
|
||||||
@ -137,29 +133,33 @@ public final class ComputerThread {
|
|||||||
private final long minPeriod;
|
private final long minPeriod;
|
||||||
|
|
||||||
private final ReentrantLock computerLock = new ReentrantLock();
|
private final ReentrantLock computerLock = new ReentrantLock();
|
||||||
private final Condition workerWakeup = computerLock.newCondition();
|
private final @GuardedBy("computerLock") Condition workerWakeup = computerLock.newCondition();
|
||||||
private final Condition monitorWakeup = computerLock.newCondition();
|
private final @GuardedBy("computerLock") Condition monitorWakeup = computerLock.newCondition();
|
||||||
|
|
||||||
private final AtomicInteger idleWorkers = new AtomicInteger(0);
|
private final AtomicInteger idleWorkers = new AtomicInteger(0);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Active queues to execute.
|
* Active queues to execute.
|
||||||
*/
|
*/
|
||||||
private final TreeSet<ComputerExecutor> computerQueue = new TreeSet<>((a, b) -> {
|
@GuardedBy("computerLock")
|
||||||
|
private final TreeSet<ExecutorImpl> computerQueue = new TreeSet<>(ComputerThread::compareExecutors);
|
||||||
|
|
||||||
|
@SuppressWarnings("GuardedBy")
|
||||||
|
private static int compareExecutors(ExecutorImpl a, ExecutorImpl b) {
|
||||||
if (a == b) return 0; // Should never happen, but let's be consistent here
|
if (a == b) return 0; // Should never happen, but let's be consistent here
|
||||||
|
|
||||||
long at = a.virtualRuntime, bt = b.virtualRuntime;
|
long at = a.virtualRuntime, bt = b.virtualRuntime;
|
||||||
if (at == bt) return Integer.compare(a.hashCode(), b.hashCode());
|
if (at == bt) return Integer.compare(a.hashCode(), b.hashCode());
|
||||||
return at < bt ? -1 : 1;
|
return at < bt ? -1 : 1;
|
||||||
});
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The minimum {@link ComputerExecutor#virtualRuntime} time on the tree.
|
* The minimum {@link ExecutorImpl#virtualRuntime} time on the tree.
|
||||||
*/
|
*/
|
||||||
private long minimumVirtualRuntime = 0;
|
private long minimumVirtualRuntime = 0;
|
||||||
|
|
||||||
public ComputerThread(int threadCount) {
|
public ComputerThread(int threadCount) {
|
||||||
workers = new Worker[threadCount];
|
workers = new WorkerThread[threadCount];
|
||||||
|
|
||||||
// latency and minPeriod are scaled by 1 + floor(log2(threads)). We can afford to execute tasks for
|
// latency and minPeriod are scaled by 1 + floor(log2(threads)). We can afford to execute tasks for
|
||||||
// longer when executing on more than one thread.
|
// longer when executing on more than one thread.
|
||||||
@ -168,13 +168,28 @@ public final class ComputerThread {
|
|||||||
minPeriod = DEFAULT_MIN_PERIOD * factor;
|
minPeriod = DEFAULT_MIN_PERIOD * factor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Executor createExecutor(ComputerScheduler.Worker worker, MetricsObserver metrics) {
|
||||||
|
return new ExecutorImpl(worker, metrics);
|
||||||
|
}
|
||||||
|
|
||||||
@GuardedBy("threadLock")
|
@GuardedBy("threadLock")
|
||||||
private void addWorker(int index) {
|
private void addWorker(int index) {
|
||||||
LOG.trace("Spawning new worker {}.", index);
|
LOG.trace("Spawning new worker {}.", index);
|
||||||
(workers[index] = new Worker(index)).owner.start();
|
(workers[index] = new WorkerThread(index)).owner.start();
|
||||||
workerCount++;
|
workerCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("GuardedBy")
|
||||||
|
private int workerCount() {
|
||||||
|
return workerCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("GuardedBy")
|
||||||
|
private WorkerThread[] workersReadOnly() {
|
||||||
|
return workers;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure sufficient workers are running.
|
* Ensure sufficient workers are running.
|
||||||
*/
|
*/
|
||||||
@ -182,7 +197,7 @@ public final class ComputerThread {
|
|||||||
private void ensureRunning() {
|
private void ensureRunning() {
|
||||||
// Don't even enter the lock if we've a monitor and don't need to/can't spawn an additional worker.
|
// Don't even enter the lock if we've a monitor and don't need to/can't spawn an additional worker.
|
||||||
// We'll be holding the computer lock at this point, so there's no problems with idleWorkers being wrong.
|
// We'll be holding the computer lock at this point, so there's no problems with idleWorkers being wrong.
|
||||||
if (monitor != null && (idleWorkers.get() > 0 || workerCount == workers.length)) return;
|
if (monitor != null && (idleWorkers.get() > 0 || workerCount() == workersReadOnly().length)) return;
|
||||||
|
|
||||||
threadLock.lock();
|
threadLock.lock();
|
||||||
try {
|
try {
|
||||||
@ -217,6 +232,7 @@ public final class ComputerThread {
|
|||||||
* @return Whether the thread was successfully shut down.
|
* @return Whether the thread was successfully shut down.
|
||||||
* @throws InterruptedException If interrupted while waiting.
|
* @throws InterruptedException If interrupted while waiting.
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
public boolean stop(long timeout, TimeUnit unit) throws InterruptedException {
|
public boolean stop(long timeout, TimeUnit unit) throws InterruptedException {
|
||||||
advanceState(STOPPING);
|
advanceState(STOPPING);
|
||||||
|
|
||||||
@ -269,12 +285,12 @@ public final class ComputerThread {
|
|||||||
/**
|
/**
|
||||||
* Mark a computer as having work, enqueuing it on the thread.
|
* Mark a computer as having work, enqueuing it on the thread.
|
||||||
* <p>
|
* <p>
|
||||||
* You must be holding {@link ComputerExecutor}'s {@code queueLock} when calling this method - it should only
|
* You must be holding {@link ExecutorImpl}'s {@code queueLock} when calling this method - it should only
|
||||||
* be called from {@code enqueue}.
|
* be called from {@code enqueue}.
|
||||||
*
|
*
|
||||||
* @param executor The computer to execute work on.
|
* @param executor The computer to execute work on.
|
||||||
*/
|
*/
|
||||||
void queue(ComputerExecutor executor) {
|
void queue(ExecutorImpl executor) {
|
||||||
computerLock.lock();
|
computerLock.lock();
|
||||||
try {
|
try {
|
||||||
if (state.get() != RUNNING) throw new IllegalStateException("ComputerThread is no longer running");
|
if (state.get() != RUNNING) throw new IllegalStateException("ComputerThread is no longer running");
|
||||||
@ -282,9 +298,6 @@ public final class ComputerThread {
|
|||||||
// Ensure we've got a worker running.
|
// Ensure we've got a worker running.
|
||||||
ensureRunning();
|
ensureRunning();
|
||||||
|
|
||||||
if (executor.onComputerQueue) throw new IllegalStateException("Cannot queue already queued executor");
|
|
||||||
executor.onComputerQueue = true;
|
|
||||||
|
|
||||||
updateRuntimes(null);
|
updateRuntimes(null);
|
||||||
|
|
||||||
// We're not currently on the queue, so update its current execution time to
|
// We're not currently on the queue, so update its current execution time to
|
||||||
@ -316,14 +329,15 @@ public final class ComputerThread {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the {@link ComputerExecutor#virtualRuntime}s of all running tasks, and then update the
|
* Update the {@link ExecutorImpl#virtualRuntime}s of all running tasks, and then update the
|
||||||
* {@link #minimumVirtualRuntime} based on the current tasks.
|
* {@link #minimumVirtualRuntime} based on the current tasks.
|
||||||
* <p>
|
* <p>
|
||||||
* This is called before queueing tasks, to ensure that {@link #minimumVirtualRuntime} is up-to-date.
|
* This is called before queueing tasks, to ensure that {@link #minimumVirtualRuntime} is up-to-date.
|
||||||
*
|
*
|
||||||
* @param current The machine which we updating runtimes from.
|
* @param current The machine which we updating runtimes from.
|
||||||
*/
|
*/
|
||||||
private void updateRuntimes(@Nullable ComputerExecutor current) {
|
@GuardedBy("computerLock")
|
||||||
|
private void updateRuntimes(@Nullable ExecutorImpl current) {
|
||||||
var minRuntime = Long.MAX_VALUE;
|
var minRuntime = Long.MAX_VALUE;
|
||||||
|
|
||||||
// If we've a task on the queue, use that as our base time.
|
// If we've a task on the queue, use that as our base time.
|
||||||
@ -332,7 +346,7 @@ public final class ComputerThread {
|
|||||||
// Update all the currently executing tasks
|
// Update all the currently executing tasks
|
||||||
var now = System.nanoTime();
|
var now = System.nanoTime();
|
||||||
var tasks = 1 + computerQueue.size();
|
var tasks = 1 + computerQueue.size();
|
||||||
for (@Nullable var runner : workers) {
|
for (@Nullable var runner : workersReadOnly()) {
|
||||||
if (runner == null) continue;
|
if (runner == null) continue;
|
||||||
var executor = runner.currentExecutor.get();
|
var executor = runner.currentExecutor.get();
|
||||||
if (executor == null) continue;
|
if (executor == null) continue;
|
||||||
@ -357,22 +371,9 @@ public final class ComputerThread {
|
|||||||
* Ensure the "currently working" state of the executor is reset, the timings are updated, and then requeue the
|
* Ensure the "currently working" state of the executor is reset, the timings are updated, and then requeue the
|
||||||
* executor if needed.
|
* executor if needed.
|
||||||
*
|
*
|
||||||
* @param runner The runner this task was on.
|
|
||||||
* @param executor The executor to requeue
|
* @param executor The executor to requeue
|
||||||
*/
|
*/
|
||||||
private void afterWork(Worker runner, ComputerExecutor executor) {
|
private void afterWork(ExecutorImpl executor) {
|
||||||
// Clear the executor's thread.
|
|
||||||
var currentThread = executor.executingThread.getAndSet(null);
|
|
||||||
if (currentThread != runner.owner) {
|
|
||||||
|
|
||||||
LOG.error(
|
|
||||||
"Expected computer #{} to be running on {}, but already running on {}. This is a SERIOUS bug, please report with your debug.log.",
|
|
||||||
executor.getComputer().getID(),
|
|
||||||
runner.owner.getName(),
|
|
||||||
currentThread == null ? "nothing" : currentThread.getName()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
computerLock.lock();
|
computerLock.lock();
|
||||||
try {
|
try {
|
||||||
updateRuntimes(executor);
|
updateRuntimes(executor);
|
||||||
@ -388,6 +389,13 @@ public final class ComputerThread {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("GuardedBy")
|
||||||
|
private int computerQueueSize() {
|
||||||
|
// FIXME: We access this on other threads (in TimeoutState), so their reads won't be consistent. This isn't
|
||||||
|
// "critical" behaviour, so not clear if it matters too much.
|
||||||
|
return computerQueue.size();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The scaled period for a single task.
|
* The scaled period for a single task.
|
||||||
*
|
*
|
||||||
@ -397,11 +405,8 @@ public final class ComputerThread {
|
|||||||
* @see #LATENCY_MAX_TASKS
|
* @see #LATENCY_MAX_TASKS
|
||||||
*/
|
*/
|
||||||
long scaledPeriod() {
|
long scaledPeriod() {
|
||||||
// FIXME: We access this on other threads (in TimeoutState), so their reads won't be consistent. This isn't
|
|
||||||
// "critical" behaviour, so not clear if it matters too much.
|
|
||||||
|
|
||||||
// +1 to include the current task
|
// +1 to include the current task
|
||||||
var count = 1 + computerQueue.size();
|
var count = 1 + computerQueueSize();
|
||||||
return count < LATENCY_MAX_TASKS ? latency / count : minPeriod;
|
return count < LATENCY_MAX_TASKS ? latency / count : minPeriod;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -411,9 +416,8 @@ public final class ComputerThread {
|
|||||||
* @return If we have work queued up.
|
* @return If we have work queued up.
|
||||||
*/
|
*/
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
public boolean hasPendingWork() {
|
boolean hasPendingWork() {
|
||||||
// FIXME: See comment in scaledPeriod. Again, we access this in multiple threads but not clear if it matters!
|
return computerQueueSize() > 0;
|
||||||
return !computerQueue.isEmpty();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -422,12 +426,11 @@ public final class ComputerThread {
|
|||||||
*
|
*
|
||||||
* @return If the computer threads are busy.
|
* @return If the computer threads are busy.
|
||||||
*/
|
*/
|
||||||
@GuardedBy("computerLock")
|
|
||||||
private boolean isBusy() {
|
private boolean isBusy() {
|
||||||
return computerQueue.size() > idleWorkers.get();
|
return computerQueueSize() > idleWorkers.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void workerFinished(Worker worker) {
|
private void workerFinished(WorkerThread worker) {
|
||||||
// We should only shut down a worker once! This should only happen if we fail to abort a worker and then the
|
// We should only shut down a worker once! This should only happen if we fail to abort a worker and then the
|
||||||
// worker finishes normally.
|
// worker finishes normally.
|
||||||
if (!worker.running.getAndSet(false)) return;
|
if (!worker.running.getAndSet(false)) return;
|
||||||
@ -442,6 +445,7 @@ public final class ComputerThread {
|
|||||||
workerCount--;
|
workerCount--;
|
||||||
|
|
||||||
if (workers[worker.index] != worker) {
|
if (workers[worker.index] != worker) {
|
||||||
|
assert false : "workerFinished but inconsistent worker";
|
||||||
LOG.error("Worker {} closed, but new runner has been spawned.", worker.index);
|
LOG.error("Worker {} closed, but new runner has been spawned.", worker.index);
|
||||||
} else if (state.get() == RUNNING || (state.get() == STOPPING && hasPendingWork())) {
|
} else if (state.get() == RUNNING || (state.get() == STOPPING && hasPendingWork())) {
|
||||||
addWorker(worker.index);
|
addWorker(worker.index);
|
||||||
@ -455,7 +459,7 @@ public final class ComputerThread {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observes all currently active {@link Worker}s and terminates their tasks once they have exceeded the hard
|
* Observes all currently active {@link WorkerThread}s and terminates their tasks once they have exceeded the hard
|
||||||
* abort limit.
|
* abort limit.
|
||||||
*
|
*
|
||||||
* @see TimeoutState
|
* @see TimeoutState
|
||||||
@ -491,7 +495,7 @@ public final class ComputerThread {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void checkRunners() {
|
private void checkRunners() {
|
||||||
for (@Nullable var runner : workers) {
|
for (@Nullable var runner : workersReadOnly()) {
|
||||||
if (runner == null) continue;
|
if (runner == null) continue;
|
||||||
|
|
||||||
// If the worker has no work, skip
|
// If the worker has no work, skip
|
||||||
@ -503,25 +507,28 @@ public final class ComputerThread {
|
|||||||
|
|
||||||
// If we're still within normal execution times (TIMEOUT) or soft abort (ABORT_TIMEOUT),
|
// If we're still within normal execution times (TIMEOUT) or soft abort (ABORT_TIMEOUT),
|
||||||
// then we can let the Lua machine do its work.
|
// then we can let the Lua machine do its work.
|
||||||
var afterStart = executor.timeout.nanoCumulative();
|
var remainingTime = executor.timeout.getRemainingTime();
|
||||||
var afterHardAbort = afterStart - TimeoutState.TIMEOUT - TimeoutState.ABORT_TIMEOUT;
|
// If remainingTime > 0, then we're executing normally,
|
||||||
|
// If remainingTime > -ABORT_TIMEOUT, then we've soft aborted.
|
||||||
|
// Otherwise, remainingTime <= -ABORT_TIMEOUT, and we've run over by -ABORT_TIMEOUT - remainingTime.
|
||||||
|
var afterHardAbort = -remainingTime - TimeoutState.ABORT_TIMEOUT;
|
||||||
if (afterHardAbort < 0) continue;
|
if (afterHardAbort < 0) continue;
|
||||||
|
|
||||||
// Set the hard abort flag.
|
// Set the hard abort flag.
|
||||||
executor.timeout.hardAbort();
|
executor.timeout.hardAbort();
|
||||||
executor.abort();
|
executor.worker.abortWithTimeout();
|
||||||
|
|
||||||
if (afterHardAbort >= TimeoutState.ABORT_TIMEOUT * 2) {
|
if (afterHardAbort >= TimeoutState.ABORT_TIMEOUT * 2) {
|
||||||
// If we've hard aborted and interrupted, and we're still not dead, then mark the worker
|
// If we've hard aborted and interrupted, and we're still not dead, then mark the worker
|
||||||
// as dead, finish off the task, and spawn a new runner.
|
// as dead, finish off the task, and spawn a new runner.
|
||||||
runner.reportTimeout(executor, afterStart);
|
runner.reportTimeout(executor, remainingTime);
|
||||||
runner.owner.interrupt();
|
runner.owner.interrupt();
|
||||||
|
|
||||||
workerFinished(runner);
|
workerFinished(runner);
|
||||||
} else if (afterHardAbort >= TimeoutState.ABORT_TIMEOUT) {
|
} else if (afterHardAbort >= TimeoutState.ABORT_TIMEOUT) {
|
||||||
// If we've hard aborted but we're still not dead, dump the stack trace and interrupt
|
// If we've hard aborted but we're still not dead, dump the stack trace and interrupt
|
||||||
// the task.
|
// the task.
|
||||||
runner.reportTimeout(executor, afterStart);
|
runner.reportTimeout(executor, remainingTime);
|
||||||
runner.owner.interrupt();
|
runner.owner.interrupt();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -531,11 +538,11 @@ public final class ComputerThread {
|
|||||||
/**
|
/**
|
||||||
* Pulls tasks from the {@link #computerQueue} queue and runs them.
|
* Pulls tasks from the {@link #computerQueue} queue and runs them.
|
||||||
* <p>
|
* <p>
|
||||||
* This is responsible for running the {@link ComputerExecutor#work()}, {@link ComputerExecutor#beforeWork()} and
|
* This is responsible for running the {@link ComputerScheduler.Worker#work()}, {@link ExecutorImpl#beforeWork()}
|
||||||
* {@link ComputerExecutor#afterWork()} functions. Everything else is either handled by the executor, timeout
|
* and {@link ExecutorImpl#afterWork()} functions. Everything else is either handled by the executor,
|
||||||
* state or monitor.
|
* timeout state or monitor.
|
||||||
*/
|
*/
|
||||||
private final class Worker implements Runnable {
|
private final class WorkerThread implements Runnable {
|
||||||
/**
|
/**
|
||||||
* The index into the {@link #workers} array.
|
* The index into the {@link #workers} array.
|
||||||
*/
|
*/
|
||||||
@ -550,21 +557,21 @@ public final class ComputerThread {
|
|||||||
* Whether this runner is currently executing. This may be set to false when this worker terminates, or when
|
* Whether this runner is currently executing. This may be set to false when this worker terminates, or when
|
||||||
* we try to abandon a worker in the monitor
|
* we try to abandon a worker in the monitor
|
||||||
*
|
*
|
||||||
* @see #workerFinished(Worker)
|
* @see #workerFinished(WorkerThread)
|
||||||
*/
|
*/
|
||||||
final AtomicBoolean running = new AtomicBoolean(true);
|
final AtomicBoolean running = new AtomicBoolean(true);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The computer we're currently running.
|
* The computer we're currently running.
|
||||||
*/
|
*/
|
||||||
final AtomicReference<ComputerExecutor> currentExecutor = new AtomicReference<>(null);
|
final AtomicReference<ExecutorImpl> currentExecutor = new AtomicReference<>(null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The last time we reported a stack trace, used to avoid spamming the logs.
|
* The last time we reported a stack trace, used to avoid spamming the logs.
|
||||||
*/
|
*/
|
||||||
AtomicLong lastReport = new AtomicLong(Long.MIN_VALUE);
|
AtomicLong lastReport = new AtomicLong(Long.MIN_VALUE);
|
||||||
|
|
||||||
Worker(int index) {
|
WorkerThread(int index) {
|
||||||
this.index = index;
|
this.index = index;
|
||||||
owner = workerFactory.newThread(this);
|
owner = workerFactory.newThread(this);
|
||||||
}
|
}
|
||||||
@ -579,10 +586,9 @@ public final class ComputerThread {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void runImpl() {
|
private void runImpl() {
|
||||||
tasks:
|
|
||||||
while (running.get()) {
|
while (running.get()) {
|
||||||
// Wait for an active queue to execute
|
// Wait for an active queue to execute
|
||||||
ComputerExecutor executor;
|
ExecutorImpl executor;
|
||||||
computerLock.lock();
|
computerLock.lock();
|
||||||
try {
|
try {
|
||||||
idleWorkers.getAndIncrement();
|
idleWorkers.getAndIncrement();
|
||||||
@ -597,21 +603,18 @@ public final class ComputerThread {
|
|||||||
computerLock.unlock();
|
computerLock.unlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're trying to executing some task on this computer while someone else is doing work, something
|
// Mark this computer as executing.
|
||||||
// is seriously wrong.
|
if (!ExecutorImpl.STATE.compareAndSet(executor, ExecutorState.ON_QUEUE, ExecutorState.RUNNING)) {
|
||||||
while (!executor.executingThread.compareAndSet(null, owner)) {
|
assert false : "Running computer on the wrong thread";
|
||||||
var existing = executor.executingThread.get();
|
LOG.error(
|
||||||
if (existing != null) {
|
"Trying to run computer #{} on thread {}, but already running on another thread. This is a SERIOUS " +
|
||||||
LOG.error(
|
"bug, please report with your debug.log.",
|
||||||
"Trying to run computer #{} on thread {}, but already running on {}. This is a SERIOUS bug, please report with your debug.log.",
|
executor.worker.getComputerID(), owner.getName()
|
||||||
executor.getComputer().getID(), owner.getName(), existing.getName()
|
);
|
||||||
);
|
|
||||||
continue tasks;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're stopping, the only thing this executor should be doing is shutting down.
|
// If we're stopping, the only thing this executor should be doing is shutting down.
|
||||||
if (state.get() >= STOPPING) executor.queueStop(false, true);
|
if (state.get() >= STOPPING) executor.worker.unload();
|
||||||
|
|
||||||
// Reset the timers
|
// Reset the timers
|
||||||
executor.beforeWork();
|
executor.beforeWork();
|
||||||
@ -622,19 +625,19 @@ public final class ComputerThread {
|
|||||||
|
|
||||||
// Execute the task
|
// Execute the task
|
||||||
try {
|
try {
|
||||||
executor.work();
|
executor.worker.work();
|
||||||
} catch (Exception | LinkageError | VirtualMachineError e) {
|
} catch (Exception | LinkageError | VirtualMachineError e) {
|
||||||
LOG.error("Error running task on computer #" + executor.getComputer().getID(), e);
|
LOG.error("Error running task on computer #" + executor.worker.getComputerID(), e);
|
||||||
// Tear down the computer immediately. There's no guarantee it's well-behaved from now on.
|
// Tear down the computer immediately. There's no guarantee it's well-behaved from now on.
|
||||||
executor.fastFail();
|
executor.worker.abortWithError();
|
||||||
} finally {
|
} finally {
|
||||||
var thisExecutor = currentExecutor.getAndSet(null);
|
var thisExecutor = currentExecutor.getAndSet(null);
|
||||||
if (thisExecutor != null) afterWork(this, executor);
|
if (thisExecutor != null) afterWork(executor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void reportTimeout(ComputerExecutor executor, long time) {
|
private void reportTimeout(ExecutorImpl executor, long time) {
|
||||||
if (!LOG.isErrorEnabled(Logging.COMPUTER_ERROR)) return;
|
if (!LOG.isErrorEnabled(Logging.COMPUTER_ERROR)) return;
|
||||||
|
|
||||||
// Attempt to debounce stack trace reporting, limiting ourselves to one every second. There's no need to be
|
// Attempt to debounce stack trace reporting, limiting ourselves to one every second. There's no need to be
|
||||||
@ -647,8 +650,8 @@ public final class ComputerThread {
|
|||||||
var owner = Objects.requireNonNull(this.owner);
|
var owner = Objects.requireNonNull(this.owner);
|
||||||
|
|
||||||
var builder = new StringBuilder()
|
var builder = new StringBuilder()
|
||||||
.append("Terminating computer #").append(executor.getComputer().getID())
|
.append("Terminating computer #").append(executor.worker.getComputerID())
|
||||||
.append(" due to timeout (running for ").append(time * 1e-9)
|
.append(" due to timeout (ran over by ").append(time * -1e-9)
|
||||||
.append(" seconds). This is NOT a bug, but may mean a computer is misbehaving.\n")
|
.append(" seconds). This is NOT a bug, but may mean a computer is misbehaving.\n")
|
||||||
.append("Thread ")
|
.append("Thread ")
|
||||||
.append(owner.getName())
|
.append(owner.getName())
|
||||||
@ -662,9 +665,150 @@ public final class ComputerThread {
|
|||||||
builder.append(" at ").append(element).append('\n');
|
builder.append(" at ").append(element).append('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
executor.printState(builder);
|
executor.worker.writeState(builder);
|
||||||
|
|
||||||
LOG.warn(builder.toString());
|
LOG.warn(builder.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current state of a {@link ExecutorState}.
|
||||||
|
* <p>
|
||||||
|
* Executors are either enqueued (have more work to do) or not and working or not. This enum encapsulates the four
|
||||||
|
* combinations of these properties, with the following transitions:
|
||||||
|
*
|
||||||
|
* <pre>{@code
|
||||||
|
* submit() afterWork()
|
||||||
|
* IDLE ---------> ON_QUEUE <----------- REPEAT
|
||||||
|
* ^ | ^
|
||||||
|
* | | runImpl() |
|
||||||
|
* | V |
|
||||||
|
* +---------------RUNNING----------------+
|
||||||
|
* afterWork() submit()
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
|
enum ExecutorState {
|
||||||
|
/**
|
||||||
|
* This executor is idle.
|
||||||
|
*/
|
||||||
|
IDLE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This executor is on the queue but idle.
|
||||||
|
*/
|
||||||
|
ON_QUEUE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This executor is running and will transition to idle after execution.
|
||||||
|
*/
|
||||||
|
RUNNING,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This executor is running and should run again after this task finishes.
|
||||||
|
*/
|
||||||
|
REPEAT;
|
||||||
|
|
||||||
|
ExecutorState enqueue() {
|
||||||
|
return switch (this) {
|
||||||
|
case IDLE, ON_QUEUE -> ON_QUEUE;
|
||||||
|
case RUNNING, REPEAT -> REPEAT;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ExecutorState requeue() {
|
||||||
|
return switch (this) {
|
||||||
|
case IDLE, ON_QUEUE -> {
|
||||||
|
assert false : "Impossible state after executing";
|
||||||
|
LOG.error("Impossible state - calling requeue with {}.", this);
|
||||||
|
yield ExecutorState.ON_QUEUE;
|
||||||
|
}
|
||||||
|
case RUNNING -> ExecutorState.IDLE;
|
||||||
|
case REPEAT -> ExecutorState.ON_QUEUE;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class ExecutorImpl implements Executor {
|
||||||
|
public static final AtomicReferenceFieldUpdater<ExecutorImpl, ExecutorState> STATE = AtomicReferenceFieldUpdater.newUpdater(
|
||||||
|
ExecutorImpl.class, ExecutorState.class, "$state"
|
||||||
|
);
|
||||||
|
|
||||||
|
final Worker worker;
|
||||||
|
private final MetricsObserver metrics;
|
||||||
|
final TimeoutImpl timeout;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current state of this worker.
|
||||||
|
*/
|
||||||
|
private volatile ExecutorState $state = ExecutorState.IDLE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The amount of time this computer has used on a theoretical machine which shares work evenly amongst computers.
|
||||||
|
*
|
||||||
|
* @see ComputerThread
|
||||||
|
*/
|
||||||
|
long virtualRuntime = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The last time at which we updated {@link #virtualRuntime}.
|
||||||
|
*
|
||||||
|
* @see ComputerThread
|
||||||
|
*/
|
||||||
|
long vRuntimeStart;
|
||||||
|
|
||||||
|
ExecutorImpl(Worker worker, MetricsObserver metrics) {
|
||||||
|
this.worker = worker;
|
||||||
|
this.metrics = metrics;
|
||||||
|
timeout = new TimeoutImpl();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called before calling {@link Worker#work()}, setting up any important state.
|
||||||
|
*/
|
||||||
|
void beforeWork() {
|
||||||
|
vRuntimeStart = System.nanoTime();
|
||||||
|
timeout.startTimer(scaledPeriod());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called after executing {@link Worker#work()}.
|
||||||
|
*
|
||||||
|
* @return If we have more work to do.
|
||||||
|
*/
|
||||||
|
boolean afterWork() {
|
||||||
|
timeout.reset();
|
||||||
|
metrics.observe(Metrics.COMPUTER_TASKS, timeout.getExecutionTime());
|
||||||
|
|
||||||
|
var state = STATE.getAndUpdate(this, ExecutorState::requeue);
|
||||||
|
return state == ExecutorState.REPEAT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void submit() {
|
||||||
|
var state = STATE.getAndUpdate(this, ExecutorState::enqueue);
|
||||||
|
if (state == ExecutorState.IDLE) queue(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TimeoutState timeoutState() {
|
||||||
|
return timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getRemainingTime() {
|
||||||
|
return timeout.getRemainingTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRemainingTime(long time) {
|
||||||
|
timeout.setRemainingTime(time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class TimeoutImpl extends ManagedTimeoutState {
|
||||||
|
@Override
|
||||||
|
protected boolean shouldPause() {
|
||||||
|
return hasPendingWork();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -0,0 +1,127 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.core.computer.computerthread;
|
||||||
|
|
||||||
|
import dan200.computercraft.core.computer.TimeoutState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A basic {@link TimeoutState} implementation, for use by {@link ComputerScheduler} implementations.
|
||||||
|
* <p>
|
||||||
|
* While almost all {@link TimeoutState} implementations will be derived from this class, the two are intentionally kept
|
||||||
|
* separate. This class is intended for the {@link ComputerScheduler} (which is responsible for controlling the
|
||||||
|
* timeout), and not for the main computer logic, which only needs to check timeout flags.
|
||||||
|
* <p>
|
||||||
|
* This class tracks the time a computer was started (and thus {@linkplain #getExecutionTime()} how long it has been
|
||||||
|
* running for), as well as the deadline for when a computer should be soft aborted and paused.
|
||||||
|
*/
|
||||||
|
public abstract class ManagedTimeoutState extends TimeoutState {
|
||||||
|
/**
|
||||||
|
* When execution of this computer started.
|
||||||
|
*
|
||||||
|
* @see #getExecutionTime()
|
||||||
|
*/
|
||||||
|
private long startTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time when this computer should be aborted.
|
||||||
|
*
|
||||||
|
* @see #getRemainingTime()
|
||||||
|
* @see #setRemainingTime(long)
|
||||||
|
*/
|
||||||
|
private long abortDeadline;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time when this computer should be paused if {@link ComputerThread#hasPendingWork()} is set.
|
||||||
|
*/
|
||||||
|
private long pauseDeadline;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final synchronized void refresh() {
|
||||||
|
// Important: The weird arithmetic here is important, as nanoTime may return negative values, and so we
|
||||||
|
// need to handle overflow.
|
||||||
|
var now = System.nanoTime();
|
||||||
|
var changed = false;
|
||||||
|
if (!paused && Long.compareUnsigned(now, pauseDeadline) >= 0 && shouldPause()) { // now >= currentDeadline
|
||||||
|
paused = true;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (!softAbort && Long.compareUnsigned(now, abortDeadline) >= 0) { // now >= currentAbort
|
||||||
|
softAbort = true;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (softAbort && !hardAbort && Long.compareUnsigned(now, abortDeadline + ABORT_TIMEOUT) >= 0) { // now >= currentAbort + ABORT_TIMEOUT.
|
||||||
|
hardAbort = true;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) updateListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get how long this computer has been executing for.
|
||||||
|
*
|
||||||
|
* @return How long the computer has been running for in nanoseconds.
|
||||||
|
*/
|
||||||
|
public final long getExecutionTime() {
|
||||||
|
return System.nanoTime() - startTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get how long this computer is permitted to run before being aborted.
|
||||||
|
*
|
||||||
|
* @return The remaining time, in nanoseconds.
|
||||||
|
* @see ComputerScheduler.Executor#getRemainingTime()
|
||||||
|
*/
|
||||||
|
public final long getRemainingTime() {
|
||||||
|
return abortDeadline - System.nanoTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set how long this computer is permitted to run before being aborted.
|
||||||
|
*
|
||||||
|
* @param time The remaining time, in nanoseconds.
|
||||||
|
* @see ComputerScheduler.Executor#setRemainingTime(long)
|
||||||
|
*/
|
||||||
|
public final void setRemainingTime(long time) {
|
||||||
|
abortDeadline = startTime + time;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the hard-abort flag immediately.
|
||||||
|
*/
|
||||||
|
public final void hardAbort() {
|
||||||
|
softAbort = hardAbort = true;
|
||||||
|
synchronized (this) {
|
||||||
|
updateListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start this timer, recording the current start time, and deadline before a computer may be paused.
|
||||||
|
*
|
||||||
|
* @param pauseTimeout The minimum time this computer can run before potentially being paused.
|
||||||
|
*/
|
||||||
|
public final synchronized void startTimer(long pauseTimeout) {
|
||||||
|
var now = System.nanoTime();
|
||||||
|
startTime = now;
|
||||||
|
abortDeadline = now + BASE_TIMEOUT;
|
||||||
|
pauseDeadline = now + pauseTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the paused and abort flags.
|
||||||
|
*/
|
||||||
|
public final synchronized void reset() {
|
||||||
|
paused = softAbort = hardAbort = false;
|
||||||
|
updateListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if this computer should be paused, as other computers are contending for work.
|
||||||
|
*
|
||||||
|
* @return If this computer should be paused.
|
||||||
|
*/
|
||||||
|
protected abstract boolean shouldPause();
|
||||||
|
}
|
@ -0,0 +1,142 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.core.computer.computerthread;
|
||||||
|
|
||||||
|
import dan200.computercraft.core.computer.TimeoutState;
|
||||||
|
import dan200.computercraft.core.metrics.MetricsObserver;
|
||||||
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
|
||||||
|
import javax.annotation.concurrent.GuardedBy;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.locks.Condition;
|
||||||
|
import java.util.concurrent.locks.Lock;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
|
||||||
|
public class ComputerThreadRunner implements AutoCloseable {
|
||||||
|
private final ComputerThread thread;
|
||||||
|
|
||||||
|
private final Lock errorLock = new ReentrantLock();
|
||||||
|
private final @GuardedBy("errorLock") Condition hasError = errorLock.newCondition();
|
||||||
|
@GuardedBy("errorLock")
|
||||||
|
private @MonotonicNonNull Throwable error = null;
|
||||||
|
|
||||||
|
public ComputerThreadRunner() {
|
||||||
|
this.thread = new ComputerThread(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ComputerThread thread() {
|
||||||
|
return thread;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
try {
|
||||||
|
if (!thread.stop(10, TimeUnit.SECONDS)) {
|
||||||
|
throw new IllegalStateException("Failed to shutdown ComputerContext in time.");
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new IllegalStateException("Runtime thread was interrupted", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Worker createWorker(BiConsumer<ComputerScheduler.Executor, TimeoutState> action) {
|
||||||
|
return new Worker(thread, e -> action.accept(e, e.timeoutState()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void createLoopingComputer() {
|
||||||
|
new Worker(thread, e -> {
|
||||||
|
Thread.sleep(100);
|
||||||
|
e.submit();
|
||||||
|
}).executor().submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startAndWait(Worker worker) throws Exception {
|
||||||
|
worker.executor().submit();
|
||||||
|
do {
|
||||||
|
errorLock.lock();
|
||||||
|
try {
|
||||||
|
rethrowIfNeeded();
|
||||||
|
if (hasError.await(100, TimeUnit.MILLISECONDS)) rethrowIfNeeded();
|
||||||
|
} finally {
|
||||||
|
errorLock.unlock();
|
||||||
|
}
|
||||||
|
} while (worker.executed == 0 || thread.hasPendingWork());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GuardedBy("errorLock")
|
||||||
|
private void rethrowIfNeeded() throws Exception {
|
||||||
|
if (error != null) ComputerThreadRunner.<Exception>throwUnchecked0(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static <T extends Throwable> void throwUnchecked0(Throwable t) throws T {
|
||||||
|
throw (T) t;
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
private interface Task {
|
||||||
|
void run(ComputerScheduler.Executor executor) throws InterruptedException;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class Worker implements ComputerScheduler.Worker {
|
||||||
|
private final Task run;
|
||||||
|
private final ComputerScheduler.Executor executor;
|
||||||
|
volatile int executed = 0;
|
||||||
|
|
||||||
|
private Worker(ComputerScheduler scheduler, Task run) {
|
||||||
|
this.run = run;
|
||||||
|
this.executor = scheduler.createExecutor(this, MetricsObserver.discard());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ComputerScheduler.Executor executor() {
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void work() {
|
||||||
|
try {
|
||||||
|
run.run(executor);
|
||||||
|
executed++;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
errorLock.lock();
|
||||||
|
try {
|
||||||
|
if (error == null) {
|
||||||
|
error = e;
|
||||||
|
hasError.signal();
|
||||||
|
} else {
|
||||||
|
error.addSuppressed(e);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
errorLock.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e instanceof Exception || e instanceof AssertionError) return;
|
||||||
|
throwUnchecked0(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getComputerID() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeState(StringBuilder output) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void abortWithTimeout() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unload() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void abortWithError() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,11 +2,10 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package dan200.computercraft.core.computer;
|
package dan200.computercraft.core.computer.computerthread;
|
||||||
|
|
||||||
import dan200.computercraft.core.lua.MachineResult;
|
import dan200.computercraft.core.computer.TimeoutState;
|
||||||
import dan200.computercraft.test.core.ConcurrentHelpers;
|
import dan200.computercraft.test.core.ConcurrentHelpers;
|
||||||
import dan200.computercraft.test.core.computer.KotlinComputerManager;
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@ -28,11 +27,11 @@ import static org.junit.jupiter.api.Assertions.*;
|
|||||||
@Execution(ExecutionMode.CONCURRENT)
|
@Execution(ExecutionMode.CONCURRENT)
|
||||||
public class ComputerThreadTest {
|
public class ComputerThreadTest {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(ComputerThreadTest.class);
|
private static final Logger LOG = LoggerFactory.getLogger(ComputerThreadTest.class);
|
||||||
private KotlinComputerManager manager;
|
private ComputerThreadRunner manager;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
public void before() {
|
public void before() {
|
||||||
manager = new KotlinComputerManager();
|
manager = new ComputerThreadRunner();
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
@ -42,16 +41,13 @@ public class ComputerThreadTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSoftAbort() throws Exception {
|
public void testSoftAbort() throws Exception {
|
||||||
var computer = manager.create();
|
var computer = manager.createWorker((executor, timeout) -> {
|
||||||
manager.enqueue(computer, timeout -> {
|
executor.setRemainingTime(TimeoutState.TIMEOUT);
|
||||||
assertFalse(timeout.isSoftAborted(), "Should not start soft-aborted");
|
assertFalse(timeout.isSoftAborted(), "Should not start soft-aborted");
|
||||||
|
|
||||||
var delay = ConcurrentHelpers.waitUntil(timeout::isSoftAborted);
|
var delay = ConcurrentHelpers.waitUntil(timeout::isSoftAborted);
|
||||||
assertThat("Should be soft aborted", delay * 1e-9, closeTo(7, 1.0));
|
assertThat("Should be soft aborted", delay * 1e-9, closeTo(7, 1.0));
|
||||||
LOG.info("Slept for {}", delay);
|
LOG.info("Slept for {}", delay);
|
||||||
|
|
||||||
computer.shutdown();
|
|
||||||
return MachineResult.OK;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
manager.startAndWait(computer);
|
manager.startAndWait(computer);
|
||||||
@ -59,15 +55,12 @@ public class ComputerThreadTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testHardAbort() throws Exception {
|
public void testHardAbort() throws Exception {
|
||||||
var computer = manager.create();
|
var computer = manager.createWorker((executor, timeout) -> {
|
||||||
manager.enqueue(computer, timeout -> {
|
executor.setRemainingTime(TimeoutState.TIMEOUT);
|
||||||
assertFalse(timeout.isHardAborted(), "Should not start soft-aborted");
|
assertFalse(timeout.isHardAborted(), "Should not start soft-aborted");
|
||||||
|
|
||||||
assertThrows(InterruptedException.class, () -> Thread.sleep(11_000), "Sleep should be hard aborted");
|
assertThrows(InterruptedException.class, () -> Thread.sleep(11_000), "Sleep should be hard aborted");
|
||||||
assertTrue(timeout.isHardAborted(), "Thread should be hard aborted");
|
assertTrue(timeout.isHardAborted(), "Thread should be hard aborted");
|
||||||
|
|
||||||
computer.shutdown();
|
|
||||||
return MachineResult.OK;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
manager.startAndWait(computer);
|
manager.startAndWait(computer);
|
||||||
@ -75,13 +68,9 @@ public class ComputerThreadTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testNoPauseIfNoOtherMachines() throws Exception {
|
public void testNoPauseIfNoOtherMachines() throws Exception {
|
||||||
var computer = manager.create();
|
var computer = manager.createWorker((executor, timeout) -> {
|
||||||
manager.enqueue(computer, timeout -> {
|
|
||||||
var didPause = ConcurrentHelpers.waitUntil(timeout::isPaused, 5, TimeUnit.SECONDS);
|
var didPause = ConcurrentHelpers.waitUntil(timeout::isPaused, 5, TimeUnit.SECONDS);
|
||||||
assertFalse(didPause, "Machine shouldn't have paused within 5s");
|
assertFalse(didPause, "Machine shouldn't have paused within 5s");
|
||||||
|
|
||||||
computer.shutdown();
|
|
||||||
return MachineResult.OK;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
manager.startAndWait(computer);
|
manager.startAndWait(computer);
|
||||||
@ -89,18 +78,14 @@ public class ComputerThreadTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testPauseIfSomeOtherMachine() throws Exception {
|
public void testPauseIfSomeOtherMachine() throws Exception {
|
||||||
var computer = manager.create();
|
var computer = manager.createWorker((executor, timeout) -> {
|
||||||
manager.enqueue(computer, timeout -> {
|
var budget = manager.thread().scaledPeriod();
|
||||||
var budget = manager.context().computerScheduler().scaledPeriod();
|
|
||||||
assertEquals(budget, TimeUnit.MILLISECONDS.toNanos(25), "Budget should be 25ms");
|
assertEquals(budget, TimeUnit.MILLISECONDS.toNanos(25), "Budget should be 25ms");
|
||||||
|
|
||||||
var delay = ConcurrentHelpers.waitUntil(timeout::isPaused);
|
var delay = ConcurrentHelpers.waitUntil(timeout::isPaused);
|
||||||
// Linux appears to have much more accurate timing than Windows/OSX. Or at least on CI!
|
// Linux appears to have much more accurate timing than Windows/OSX. Or at least on CI!
|
||||||
var time = System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains("linux") ? 0.05 : 0.3;
|
var time = System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains("linux") ? 0.05 : 0.3;
|
||||||
assertThat("Paused within a short time", delay * 1e-9, lessThanOrEqualTo(time));
|
assertThat("Paused within a short time", delay * 1e-9, lessThanOrEqualTo(time));
|
||||||
|
|
||||||
computer.shutdown();
|
|
||||||
return MachineResult.OK;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
manager.createLoopingComputer();
|
manager.createLoopingComputer();
|
@ -1,189 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package dan200.computercraft.test.core.computer
|
|
||||||
|
|
||||||
import dan200.computercraft.api.lua.ILuaAPI
|
|
||||||
import dan200.computercraft.core.ComputerContext
|
|
||||||
import dan200.computercraft.core.computer.Computer
|
|
||||||
import dan200.computercraft.core.computer.TimeoutState
|
|
||||||
import dan200.computercraft.core.lua.MachineEnvironment
|
|
||||||
import dan200.computercraft.core.lua.MachineResult
|
|
||||||
import dan200.computercraft.core.terminal.Terminal
|
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import java.util.concurrent.locks.Lock
|
|
||||||
import java.util.concurrent.locks.ReentrantLock
|
|
||||||
|
|
||||||
typealias FakeComputerTask = (state: TimeoutState) -> MachineResult
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates "fake" computers, which just run user-defined tasks rather than Lua code.
|
|
||||||
*/
|
|
||||||
class KotlinComputerManager : AutoCloseable {
|
|
||||||
|
|
||||||
private val machines: MutableMap<Computer, Queue<FakeComputerTask>> = HashMap()
|
|
||||||
private val context = ComputerContext.builder(BasicEnvironment())
|
|
||||||
.luaFactory { env, _ -> DummyLuaMachine(env) }
|
|
||||||
.build()
|
|
||||||
private val errorLock: Lock = ReentrantLock()
|
|
||||||
private val hasError = errorLock.newCondition()
|
|
||||||
|
|
||||||
@Volatile
|
|
||||||
private var error: Throwable? = null
|
|
||||||
override fun close() {
|
|
||||||
try {
|
|
||||||
context.ensureClosed(10, TimeUnit.SECONDS)
|
|
||||||
} catch (e: InterruptedException) {
|
|
||||||
throw IllegalStateException("Runtime thread was interrupted", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun context(): ComputerContext {
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new computer which pulls from our task queue.
|
|
||||||
*
|
|
||||||
* @return The computer. This will not be started yet, you must call [Computer.turnOn] and
|
|
||||||
* [Computer.tick] to do so.
|
|
||||||
*/
|
|
||||||
fun create(): Computer {
|
|
||||||
val queue: Queue<FakeComputerTask> = ConcurrentLinkedQueue()
|
|
||||||
val computer = Computer(
|
|
||||||
context,
|
|
||||||
BasicEnvironment(),
|
|
||||||
Terminal(51, 19, true),
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
computer.addApi(QueuePassingAPI(queue)) // Inject an extra API to pass the queue to the machine.
|
|
||||||
machines[computer] = queue
|
|
||||||
return computer
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create and start a new computer which loops forever.
|
|
||||||
*/
|
|
||||||
fun createLoopingComputer() {
|
|
||||||
val computer = create()
|
|
||||||
enqueueForever(computer) {
|
|
||||||
Thread.sleep(100)
|
|
||||||
MachineResult.OK
|
|
||||||
}
|
|
||||||
computer.turnOn()
|
|
||||||
computer.tick()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enqueue a task on a computer.
|
|
||||||
*
|
|
||||||
* @param computer The computer to enqueue the work on.
|
|
||||||
* @param task The task to run.
|
|
||||||
*/
|
|
||||||
fun enqueue(computer: Computer, task: FakeComputerTask) {
|
|
||||||
machines[computer]!!.offer(task)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enqueue a repeated task on a computer. This is automatically requeued when the task finishes, meaning the task
|
|
||||||
* queue is never empty.
|
|
||||||
*
|
|
||||||
* @param computer The computer to enqueue the work on.
|
|
||||||
* @param task The task to run.
|
|
||||||
*/
|
|
||||||
private fun enqueueForever(computer: Computer, task: FakeComputerTask) {
|
|
||||||
machines[computer]!!.offer {
|
|
||||||
val result = task(it)
|
|
||||||
enqueueForever(computer, task)
|
|
||||||
computer.queueEvent("some_event", null)
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sleep for a given period, immediately propagating any exceptions thrown by a computer.
|
|
||||||
*
|
|
||||||
* @param delay The duration to sleep for.
|
|
||||||
* @param unit The time unit the duration is measured in.
|
|
||||||
* @throws Exception An exception thrown by a running computer.
|
|
||||||
*/
|
|
||||||
@Throws(Exception::class)
|
|
||||||
fun sleep(delay: Long, unit: TimeUnit?) {
|
|
||||||
errorLock.lock()
|
|
||||||
try {
|
|
||||||
rethrowIfNeeded()
|
|
||||||
if (hasError.await(delay, unit)) rethrowIfNeeded()
|
|
||||||
} finally {
|
|
||||||
errorLock.unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start a computer and wait for it to finish.
|
|
||||||
*
|
|
||||||
* @param computer The computer to wait for.
|
|
||||||
* @throws Exception An exception thrown by a running computer.
|
|
||||||
*/
|
|
||||||
@Throws(Exception::class)
|
|
||||||
fun startAndWait(computer: Computer) {
|
|
||||||
computer.turnOn()
|
|
||||||
computer.tick()
|
|
||||||
do {
|
|
||||||
sleep(100, TimeUnit.MILLISECONDS)
|
|
||||||
} while (context.computerScheduler().hasPendingWork() || computer.isOn)
|
|
||||||
|
|
||||||
rethrowIfNeeded()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(Exception::class)
|
|
||||||
private fun rethrowIfNeeded() {
|
|
||||||
val error = error ?: return
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
private class QueuePassingAPI constructor(val tasks: Queue<FakeComputerTask>) : ILuaAPI {
|
|
||||||
override fun getNames(): Array<String> = arrayOf()
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class DummyLuaMachine(private val environment: MachineEnvironment) : KotlinLuaMachine(environment) {
|
|
||||||
private val tasks: Queue<FakeComputerTask> =
|
|
||||||
environment.apis.asSequence().filterIsInstance(QueuePassingAPI::class.java).first().tasks
|
|
||||||
|
|
||||||
override fun getTask(): (suspend KotlinLuaMachine.() -> Unit)? {
|
|
||||||
try {
|
|
||||||
val task = tasks.remove()
|
|
||||||
return {
|
|
||||||
try {
|
|
||||||
task(environment.timeout)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
reportError(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
reportError(e)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun close() {}
|
|
||||||
|
|
||||||
private fun reportError(e: Throwable) {
|
|
||||||
errorLock.lock()
|
|
||||||
try {
|
|
||||||
if (error == null) {
|
|
||||||
error = e
|
|
||||||
hasError.signal()
|
|
||||||
} else {
|
|
||||||
error!!.addSuppressed(e)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
errorLock.unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e is Exception || e is AssertionError) return else throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,61 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package dan200.computercraft.core.computer;
|
|
||||||
|
|
||||||
import cc.tweaked.web.js.Callbacks;
|
|
||||||
import org.teavm.jso.browser.TimerHandler;
|
|
||||||
|
|
||||||
import java.util.ArrayDeque;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A reimplementation of {@link ComputerThread} which, well, avoids any threading!
|
|
||||||
* <p>
|
|
||||||
* This instead just exucutes work as soon as possible via {@link Callbacks#setImmediate(TimerHandler)}. Timeouts are
|
|
||||||
* instead handled via polling, see {@link cc.tweaked.web.builder.PatchCobalt}.
|
|
||||||
*/
|
|
||||||
public class TComputerThread {
|
|
||||||
private static final ArrayDeque<ComputerExecutor> executors = new ArrayDeque<>();
|
|
||||||
private final TimerHandler callback = this::workOnce;
|
|
||||||
|
|
||||||
public TComputerThread(int threads) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void queue(ComputerExecutor executor) {
|
|
||||||
if (executor.onComputerQueue) throw new IllegalStateException("Cannot queue already queued executor");
|
|
||||||
executor.onComputerQueue = true;
|
|
||||||
|
|
||||||
if (executors.isEmpty()) Callbacks.setImmediate(callback);
|
|
||||||
executors.add(executor);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void workOnce() {
|
|
||||||
var executor = executors.poll();
|
|
||||||
if (executor == null) throw new IllegalStateException("Working, but executor is null");
|
|
||||||
if (!executor.onComputerQueue) throw new IllegalArgumentException("Working but not on queue");
|
|
||||||
|
|
||||||
executor.beforeWork();
|
|
||||||
try {
|
|
||||||
executor.work();
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (executor.afterWork()) executors.push(executor);
|
|
||||||
if (!executors.isEmpty()) Callbacks.setImmediate(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean hasPendingWork() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long scaledPeriod() {
|
|
||||||
return 50 * 1_000_000L;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean stop(long timeout, TimeUnit unit) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,114 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package dan200.computercraft.core.computer.computerthread;
|
||||||
|
|
||||||
|
import cc.tweaked.web.js.Callbacks;
|
||||||
|
import dan200.computercraft.core.computer.TimeoutState;
|
||||||
|
import dan200.computercraft.core.metrics.MetricsObserver;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.teavm.jso.browser.TimerHandler;
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of {@link ComputerScheduler} which executes work as soon as possible via
|
||||||
|
* {@link Callbacks#setImmediate(TimerHandler)}.
|
||||||
|
* <p>
|
||||||
|
* Timeouts are instead handled via polling, see {@link cc.tweaked.web.builder.PatchCobalt}.
|
||||||
|
*
|
||||||
|
* @see ComputerThread
|
||||||
|
*/
|
||||||
|
public class TComputerThread implements ComputerScheduler {
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(TComputerThread.class);
|
||||||
|
private static final long SCALED_PERIOD = 50 * 1_000_000L;
|
||||||
|
|
||||||
|
private static final ArrayDeque<ExecutorImpl> executors = new ArrayDeque<>();
|
||||||
|
private static final TimerHandler callback = TComputerThread::workOnce;
|
||||||
|
|
||||||
|
public TComputerThread(int threads) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Executor createExecutor(Worker worker, MetricsObserver metrics) {
|
||||||
|
return new ExecutorImpl(worker);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void workOnce() {
|
||||||
|
var executor = executors.poll();
|
||||||
|
if (executor == null) throw new IllegalStateException("Working, but executor is null");
|
||||||
|
|
||||||
|
executor.beforeWork();
|
||||||
|
try {
|
||||||
|
executor.worker.work();
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.error("Error running computer", e);
|
||||||
|
executor.worker.abortWithError();
|
||||||
|
}
|
||||||
|
executor.afterWork();
|
||||||
|
|
||||||
|
if (!executors.isEmpty()) Callbacks.setImmediate(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean stop(long timeout, TimeUnit unit) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link Executor} for our scheduler.
|
||||||
|
*/
|
||||||
|
private static final class ExecutorImpl implements ComputerScheduler.Executor {
|
||||||
|
final ComputerScheduler.Worker worker;
|
||||||
|
private final TimeoutImpl timeout = new TimeoutImpl();
|
||||||
|
private boolean onQueue;
|
||||||
|
|
||||||
|
private ExecutorImpl(Worker worker) {
|
||||||
|
this.worker = worker;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void submit() {
|
||||||
|
if (onQueue) return;
|
||||||
|
onQueue = true;
|
||||||
|
|
||||||
|
if (executors.isEmpty()) Callbacks.setImmediate(callback);
|
||||||
|
executors.add(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void beforeWork() {
|
||||||
|
if (!onQueue) throw new IllegalArgumentException("Working but not on queue");
|
||||||
|
onQueue = false;
|
||||||
|
timeout.startTimer(SCALED_PERIOD);
|
||||||
|
}
|
||||||
|
|
||||||
|
void afterWork() {
|
||||||
|
timeout.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TimeoutState timeoutState() {
|
||||||
|
return timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getRemainingTime() {
|
||||||
|
return timeout.getRemainingTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRemainingTime(long time) {
|
||||||
|
timeout.setRemainingTime(time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class TimeoutImpl extends ManagedTimeoutState {
|
||||||
|
@Override
|
||||||
|
protected boolean shouldPause() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user