diff --git a/src/main/java/dan200/computercraft/core/ComputerContext.java b/src/main/java/dan200/computercraft/core/ComputerContext.java new file mode 100644 index 000000000..134673c22 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/ComputerContext.java @@ -0,0 +1,77 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.core; + +import dan200.computercraft.core.computer.GlobalEnvironment; +import dan200.computercraft.core.computer.mainthread.MainThreadScheduler; +import dan200.computercraft.core.lua.CobaltLuaMachine; +import dan200.computercraft.core.lua.ILuaMachine; + +/** + * The global context under which computers run. + */ +public final class ComputerContext implements AutoCloseable +{ + private final GlobalEnvironment globalEnvironment; + private final MainThreadScheduler mainThreadScheduler; + private final ILuaMachine.Factory factory; + + public ComputerContext( GlobalEnvironment globalEnvironment, MainThreadScheduler mainThreadScheduler, ILuaMachine.Factory factory ) + { + this.globalEnvironment = globalEnvironment; + this.mainThreadScheduler = mainThreadScheduler; + this.factory = factory; + } + + /** + * Create a default {@link ComputerContext} with the given global environment. + * + * @param environment The current global environment. + * @param mainThreadScheduler The main thread scheduler to use. + */ + public ComputerContext( GlobalEnvironment environment, MainThreadScheduler mainThreadScheduler ) + { + this( environment, mainThreadScheduler, CobaltLuaMachine::new ); + } + + /** + * The global environment. + * + * @return The current global environment. + */ + public GlobalEnvironment globalEnvironment() + { + return globalEnvironment; + } + + /** + * The {@link MainThreadScheduler} instance used to run main-thread tasks. + * + * @return The current main thread scheduler. + */ + public MainThreadScheduler mainThreadScheduler() + { + return mainThreadScheduler; + } + + /** + * The factory to create new Lua machines. + * + * @return The current Lua machine factory. + */ + public ILuaMachine.Factory luaFactory() + { + return factory; + } + + /** + * Close the current {@link ComputerContext}, disposing of any resources inside. + */ + @Override + public void close() + { + } +} diff --git a/src/main/java/dan200/computercraft/core/computer/Computer.java b/src/main/java/dan200/computercraft/core/computer/Computer.java index e897b25b1..d320a8795 100644 --- a/src/main/java/dan200/computercraft/core/computer/Computer.java +++ b/src/main/java/dan200/computercraft/core/computer/Computer.java @@ -7,16 +7,21 @@ package dan200.computercraft.core.computer; import com.google.common.base.Objects; import dan200.computercraft.api.lua.ILuaAPI; +import dan200.computercraft.api.lua.ILuaContext; +import dan200.computercraft.api.lua.ILuaTask; import dan200.computercraft.api.peripheral.IWorkMonitor; +import dan200.computercraft.core.ComputerContext; import dan200.computercraft.core.apis.IAPIEnvironment; +import dan200.computercraft.core.computer.mainthread.MainThreadScheduler; import dan200.computercraft.core.filesystem.FileSystem; import dan200.computercraft.core.terminal.Terminal; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; /** * Represents a computer which may exist in-world or elsewhere. - * + *
* Note, this class has several (read: far, far too many) responsibilities, so can get a little unwieldy at times. * *
* When the computer is instructed to turn on or off, or handle an event, we queue a task and register this to be * executed on the {@link ComputerThread}. Note, as we may be starting many events in a single tick, the external * cannot lock on anything which may be held for a long time. - * + *
* The executor is effectively composed of two separate queues. Firstly, we have a "single element" queue * {@link #command} which determines which state the computer should transition too. This is set by * {@link #queueStart()} and {@link #queueStop(boolean, boolean)}. - * + *
* When a computer is on, we simply push any events onto to the {@link #eventQueue}. - * + *
* Both queues are run from the {@link #work()} method, which tries to execute a command if one exists, or resumes the * machine with an event otherwise. - * + *
* 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}).
*/
final class ComputerExecutor
{
- static ILuaMachine.Factory luaFactory = CobaltLuaMachine::new;
private static final int QUEUE_LIMIT = 256;
private final Computer computer;
@@ -161,7 +160,9 @@ final class ComputerExecutor
*/
final AtomicReference
* Similar to {@link MainThreadExecutor}, the {@link MainThread} can be in one of three states: cool, hot and cooling.
* However, the implementation here is a little different:
- *
+ *
* {@link MainThread} starts cool, and runs as many tasks as it can in the current {@link #budget}ns. Any external tasks
* (those run by tile entities, etc...) will also consume the budget
- *
+ *
* Next tick, we add {@link ComputerCraft#maxMainGlobalTime} to our budget (clamp it to that value too). If we're still
* over budget, then we should not execute any work (either as part of {@link MainThread} or externally).
*/
-public final class MainThread
+public final class MainThread implements MainThreadScheduler
{
- /**
- * An internal counter for {@link ILuaTask} ids.
- *
- * @see dan200.computercraft.api.lua.ILuaContext#issueMainThreadTask(ILuaTask)
- * @see #getUniqueTaskID()
- */
- private static final AtomicLong lastTaskId = new AtomicLong();
-
/**
* The queue of {@link MainThreadExecutor}s with tasks to perform.
*/
- private static final TreeSet
* This is true iff {@code MAX_TICK_TIME - currentTime} was true at the beginning of the tick.
*/
- private static boolean canExecute = true;
+ private boolean canExecute = true;
- private static long minimumTime = 0;
+ private long minimumTime = 0;
- private MainThread() {}
-
- public static long getUniqueTaskID()
+ public MainThread()
{
- return lastTaskId.incrementAndGet();
}
- static void queue( @Nonnull MainThreadExecutor executor, boolean sleeper )
+ void queue( MainThreadExecutor executor )
{
synchronized( executors )
{
@@ -105,27 +92,27 @@ public final class MainThread
}
}
- static void cooling( @Nonnull MainThreadExecutor executor )
+ void cooling( MainThreadExecutor executor )
{
cooling.add( executor );
}
- static void consumeTime( long time )
+ void consumeTime( long time )
{
budget -= time;
}
- static boolean canExecute()
+ boolean canExecute()
{
return canExecute;
}
- static int currentTick()
+ int currentTick()
{
return currentTick;
}
- public static void executePendingTasks()
+ public void tick()
{
// Move onto the next tick and cool down the global executor. We're allowed to execute if we have _any_ time
// allocated for this tick. This means we'll stick much closer to doing MAX_TICK_TIME work every tick.
@@ -178,17 +165,9 @@ public final class MainThread
consumeTime( System.nanoTime() - start );
}
- public static void reset()
+ @Override
+ public Executor createExecutor( MetricsObserver metrics )
{
- currentTick = 0;
- budget = 0;
- canExecute = true;
- minimumTime = 0;
- lastTaskId.set( 0 );
- cooling.clear();
- synchronized( executors )
- {
- executors.clear();
- }
+ return new MainThreadExecutor( metrics, this );
}
}
diff --git a/src/main/java/dan200/computercraft/core/computer/MainThreadExecutor.java b/src/main/java/dan200/computercraft/core/computer/mainthread/MainThreadExecutor.java
similarity index 80%
rename from src/main/java/dan200/computercraft/core/computer/MainThreadExecutor.java
rename to src/main/java/dan200/computercraft/core/computer/mainthread/MainThreadExecutor.java
index e37e77367..8ab783267 100644
--- a/src/main/java/dan200/computercraft/core/computer/MainThreadExecutor.java
+++ b/src/main/java/dan200/computercraft/core/computer/mainthread/MainThreadExecutor.java
@@ -3,16 +3,14 @@
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
-package dan200.computercraft.core.computer;
+package dan200.computercraft.core.computer.mainthread;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.api.peripheral.IWorkMonitor;
+import dan200.computercraft.core.computer.Computer;
import dan200.computercraft.core.metrics.Metrics;
import dan200.computercraft.core.metrics.MetricsObserver;
-import dan200.computercraft.shared.turtle.core.TurtleBrain;
-import net.minecraft.tileentity.TileEntity;
-import javax.annotation.Nonnull;
import java.util.ArrayDeque;
import java.util.Queue;
import java.util.concurrent.TimeUnit;
@@ -20,28 +18,28 @@ import java.util.concurrent.TimeUnit;
/**
* Keeps track of tasks that a {@link Computer} should run on the main thread and how long that has been spent executing
* them.
- *
+ *
* This provides rate-limiting mechanism for tasks enqueued with {@link Computer#queueMainThread(Runnable)}, but also
- * those run elsewhere (such as during the turtle's tick - see {@link TurtleBrain#update()}). In order to handle this,
- * the executor goes through three stages:
- *
+ * those run elsewhere (such as during the turtle's tick). In order to handle this, the executor goes through three
+ * stages:
+ *
* When {@link State#COOL}, the computer is allocated {@link ComputerCraft#maxMainComputerTime}ns to execute any work
* this tick. At the beginning of the tick, we execute as many {@link MainThread} tasks as possible, until our
* time-frame or the global time frame has expired.
- *
- * Then, when other objects (such as {@link TileEntity}) are ticked, we update how much time we've used using
+ *
+ * Then, when other objects (such as block entities) are ticked, we update how much time we've used using
* {@link IWorkMonitor#trackWork(long, TimeUnit)}.
- *
+ *
* Now, if anywhere during this period, we use more than our allocated time slice, the executor is marked as
* {@link State#HOT}. This means it will no longer be able to execute {@link MainThread} tasks (though will still
* execute tile entity tasks, in order to prevent the main thread from exhausting work every tick).
- *
+ *
* At the beginning of the next tick, we increment the budget e by {@link ComputerCraft#maxMainComputerTime} and any
- * {@link State#HOT} executors are marked as {@link State#COOLING}. They will remain cooling until their budget is
- * fully replenished (is equal to {@link ComputerCraft#maxMainComputerTime}). Note, this is different to
- * {@link MainThread}, which allows running when it has any budget left. When cooling, no tasks are executed -
- * be they on the tile entity or main thread.
- *
+ * {@link State#HOT} executors are marked as {@link State#COOLING}. They will remain cooling until their budget is fully
+ * replenished (is equal to {@link ComputerCraft#maxMainComputerTime}). Note, this is different to {@link MainThread},
+ * which allows running when it has any budget left. When cooling, no tasks are executed - be they on the tile
+ * entity or main thread.
+ *
* This mechanism means that, on average, computers will use at most {@link ComputerCraft#maxMainComputerTime}ns per
* second, but one task source will not prevent others from executing.
*
@@ -50,7 +48,7 @@ import java.util.concurrent.TimeUnit;
* @see Computer#getMainThreadMonitor()
* @see Computer#queueMainThread(Runnable)
*/
-final class MainThreadExecutor implements IWorkMonitor
+final class MainThreadExecutor implements MainThreadScheduler.Executor
{
/**
* The maximum number of {@link MainThread} tasks allowed on the queue.
@@ -74,7 +72,7 @@ final class MainThreadExecutor implements IWorkMonitor
/**
* Determines if this executor is currently present on the queue.
- *
+ *
* This should be true iff {@link #tasks} is non-empty.
*
* @see #queueLock
@@ -110,9 +108,12 @@ final class MainThreadExecutor implements IWorkMonitor
long virtualTime;
- MainThreadExecutor( MetricsObserver metrics )
+ private final MainThread scheduler;
+
+ MainThreadExecutor( MetricsObserver metrics, MainThread scheduler )
{
this.metrics = metrics;
+ this.scheduler = scheduler;
}
/**
@@ -121,12 +122,13 @@ final class MainThreadExecutor implements IWorkMonitor
* @param runnable The task to run on the main thread.
* @return Whether this task was enqueued (namely, was there space).
*/
- boolean enqueue( Runnable runnable )
+ @Override
+ public boolean enqueue( Runnable runnable )
{
synchronized( queueLock )
{
if( tasks.size() >= MAX_TASKS || !tasks.offer( runnable ) ) return false;
- if( !onQueue && state == State.COOL ) MainThread.queue( this, true );
+ if( !onQueue && state == State.COOL ) scheduler.queue( this );
return true;
}
}
@@ -171,17 +173,17 @@ final class MainThreadExecutor implements IWorkMonitor
@Override
public boolean canWork()
{
- return state != State.COOLING && MainThread.canExecute();
+ return state != State.COOLING && scheduler.canExecute();
}
@Override
public boolean shouldWork()
{
- return state == State.COOL && MainThread.canExecute();
+ return state == State.COOL && scheduler.canExecute();
}
@Override
- public void trackWork( long time, @Nonnull TimeUnit unit )
+ public void trackWork( long time, TimeUnit unit )
{
long nanoTime = unit.toNanos( time );
synchronized( queueLock )
@@ -190,7 +192,7 @@ final class MainThreadExecutor implements IWorkMonitor
}
consumeTime( nanoTime );
- MainThread.consumeTime( nanoTime );
+ scheduler.consumeTime( nanoTime );
}
private void consumeTime( long time )
@@ -199,9 +201,9 @@ final class MainThreadExecutor implements IWorkMonitor
// Reset the budget if moving onto a new tick. We know this is safe, as this will only have happened if
// #tickCooling() isn't called, and so we didn't overrun the previous tick.
- if( currentTick != MainThread.currentTick() )
+ if( currentTick != scheduler.currentTick() )
{
- currentTick = MainThread.currentTick();
+ currentTick = scheduler.currentTick();
budget = ComputerCraft.maxMainComputerTime;
}
@@ -211,7 +213,7 @@ final class MainThreadExecutor implements IWorkMonitor
if( budget < 0 && state == State.COOL )
{
state = State.HOT;
- MainThread.cooling( this );
+ scheduler.cooling( this );
}
}
@@ -223,14 +225,14 @@ final class MainThreadExecutor implements IWorkMonitor
boolean tickCooling()
{
state = State.COOLING;
- currentTick = MainThread.currentTick();
+ currentTick = scheduler.currentTick();
budget = Math.min( budget + ComputerCraft.maxMainComputerTime, ComputerCraft.maxMainComputerTime );
if( budget < ComputerCraft.maxMainComputerTime ) return false;
state = State.COOL;
synchronized( queueLock )
{
- if( !tasks.isEmpty() && !onQueue ) MainThread.queue( this, false );
+ if( !tasks.isEmpty() && !onQueue ) scheduler.queue( this );
}
return true;
}
diff --git a/src/main/java/dan200/computercraft/core/computer/mainthread/MainThreadScheduler.java b/src/main/java/dan200/computercraft/core/computer/mainthread/MainThreadScheduler.java
new file mode 100644
index 000000000..36d7baeb5
--- /dev/null
+++ b/src/main/java/dan200/computercraft/core/computer/mainthread/MainThreadScheduler.java
@@ -0,0 +1,43 @@
+/*
+ * This file is part of ComputerCraft - http://www.computercraft.info
+ * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
+ * Send enquiries to dratcliffe@gmail.com
+ */
+package dan200.computercraft.core.computer.mainthread;
+
+import dan200.computercraft.api.peripheral.IWorkMonitor;
+import dan200.computercraft.core.metrics.MetricsObserver;
+
+import java.util.OptionalLong;
+
+/**
+ * A {@link MainThreadScheduler} is responsible for running work on the main thread, for instance the server thread in
+ * Minecraft.
+ *
+ * @see MainThread is the default implementation
+ */
+public interface MainThreadScheduler
+{
+ /**
+ * Create an executor for a computer. This should only be called once for a single computer.
+ *
+ * @param observer A sink for metrics, used to monitor task timings.
+ * @return The executor for this computer.
+ */
+ Executor createExecutor( MetricsObserver observer );
+
+ /**
+ * An {@link Executor} is responsible for managing scheduled tasks for a single computer.
+ */
+ interface Executor extends IWorkMonitor
+ {
+ /**
+ * Schedule a task to be run on the main thread. This can be called from any thread.
+ *
+ * @param task The task to schedule.
+ * @return The task ID if the task could be scheduled, or {@link OptionalLong#empty()} if the task failed to
+ * be scheduled.
+ */
+ boolean enqueue( Runnable task );
+ }
+}
diff --git a/src/main/java/dan200/computercraft/shared/CommonHooks.java b/src/main/java/dan200/computercraft/shared/CommonHooks.java
index a9c5547b5..e1cbbdd06 100644
--- a/src/main/java/dan200/computercraft/shared/CommonHooks.java
+++ b/src/main/java/dan200/computercraft/shared/CommonHooks.java
@@ -7,7 +7,6 @@ package dan200.computercraft.shared;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.core.apis.http.NetworkUtils;
-import dan200.computercraft.core.computer.MainThread;
import dan200.computercraft.core.filesystem.ResourceMount;
import dan200.computercraft.shared.command.CommandComputerCraft;
import dan200.computercraft.shared.computer.core.ServerContext;
@@ -51,8 +50,7 @@ public final class CommonHooks
{
if( event.phase == TickEvent.Phase.START )
{
- MainThread.executePendingTasks();
- ServerContext.get( ServerLifecycleHooks.getCurrentServer() ).registry().update();
+ ServerContext.get( ServerLifecycleHooks.getCurrentServer() ).tick();
}
}
@@ -85,7 +83,6 @@ public final class CommonHooks
private static void resetState()
{
ServerContext.close();
- MainThread.reset();
WirelessNetwork.resetNetworks();
NetworkUtils.reset();
}
diff --git a/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java b/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java
index 3344fe541..86e661aa7 100644
--- a/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java
+++ b/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java
@@ -57,7 +57,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment
terminal = new Terminal( terminalWidth, terminalHeight, family != ComputerFamily.NORMAL, this::markTerminalChanged );
metrics = context.metrics().createMetricObserver( this );
- computer = new Computer( context.environment(), this, terminal, computerID );
+ computer = new Computer( context.computerContext(), this, terminal, computerID );
computer.setLabel( label );
}
diff --git a/src/main/java/dan200/computercraft/shared/computer/core/ServerComputerRegistry.java b/src/main/java/dan200/computercraft/shared/computer/core/ServerComputerRegistry.java
index f99cab6ff..8ab52ab5c 100644
--- a/src/main/java/dan200/computercraft/shared/computer/core/ServerComputerRegistry.java
+++ b/src/main/java/dan200/computercraft/shared/computer/core/ServerComputerRegistry.java
@@ -43,7 +43,7 @@ public class ServerComputerRegistry
return sessionId == this.sessionId ? get( instanceId ) : null;
}
- public void update()
+ void update()
{
Iterator