mirror of
				https://github.com/SquidDev-CC/CC-Tweaked
				synced 2025-10-31 13:42:59 +00:00 
			
		
		
		
	Merge branch 'feature/optimise-timeouts' into mc-1.16.x
This commit is contained in:
		| @@ -53,6 +53,7 @@ import java.util.concurrent.locks.ReentrantLock; | ||||
|  */ | ||||
| final class ComputerExecutor | ||||
| { | ||||
|     static ILuaMachine.Factory luaFactory = CobaltLuaMachine::new; | ||||
|     private static final int QUEUE_LIMIT = 256; | ||||
| 
 | ||||
|     private final Computer computer; | ||||
| @@ -400,7 +401,7 @@ final class ComputerExecutor | ||||
|         } | ||||
| 
 | ||||
|         // Create the lua machine | ||||
|         ILuaMachine machine = new CobaltLuaMachine( computer, timeout ); | ||||
|         ILuaMachine machine = luaFactory.create( computer, timeout ); | ||||
| 
 | ||||
|         // Add the APIs. We unwrap them (yes, this is horrible) to get access to the underlying object. | ||||
|         for( ILuaAPI api : apis ) machine.addAPI( api instanceof ApiWrapper ? ((ApiWrapper) api).getDelegate() : api ); | ||||
|   | ||||
| @@ -13,6 +13,7 @@ import javax.annotation.Nullable; | ||||
| import java.util.TreeSet; | ||||
| import java.util.concurrent.ThreadFactory; | ||||
| import java.util.concurrent.TimeUnit; | ||||
| import java.util.concurrent.atomic.AtomicInteger; | ||||
| import java.util.concurrent.atomic.AtomicReference; | ||||
| import java.util.concurrent.locks.Condition; | ||||
| import java.util.concurrent.locks.LockSupport; | ||||
| @@ -49,11 +50,11 @@ import static dan200.computercraft.core.computer.TimeoutState.TIMEOUT; | ||||
| public final class ComputerThread | ||||
| { | ||||
|     /** | ||||
|      * How often the computer thread monitor should run, in milliseconds. | ||||
|      * How often the computer thread monitor should run. | ||||
|      * | ||||
|      * @see Monitor | ||||
|      */ | ||||
|     private static final int MONITOR_WAKEUP = 100; | ||||
|     private static final long MONITOR_WAKEUP = TimeUnit.MILLISECONDS.toNanos( 100 ); | ||||
| 
 | ||||
|     /** | ||||
|      * The target latency between executing two tasks on a single machine. | ||||
| @@ -76,6 +77,13 @@ public final class ComputerThread | ||||
|      */ | ||||
|     private static final long LATENCY_MAX_TASKS = DEFAULT_LATENCY / DEFAULT_MIN_PERIOD; | ||||
| 
 | ||||
|     /** | ||||
|      * Time difference between reporting crashed threads. | ||||
|      * | ||||
|      * @see TaskRunner#reportTimeout(ComputerExecutor, long) | ||||
|      */ | ||||
|     private static final long REPORT_DEBOUNCE = TimeUnit.SECONDS.toNanos( 1 ); | ||||
| 
 | ||||
|     /** | ||||
|      * Lock used for modifications to the array of current threads. | ||||
|      */ | ||||
| @@ -102,6 +110,8 @@ public final class ComputerThread | ||||
|     private static final ReentrantLock computerLock = new ReentrantLock(); | ||||
| 
 | ||||
|     private static final Condition hasWork = computerLock.newCondition(); | ||||
|     private static final AtomicInteger idleWorkers = new AtomicInteger( 0 ); | ||||
|     private static final Condition monitorWakeup = computerLock.newCondition(); | ||||
| 
 | ||||
|     /** | ||||
|      * Active queues to execute. | ||||
| @@ -135,7 +145,7 @@ public final class ComputerThread | ||||
| 
 | ||||
|             if( runners == null ) | ||||
|             { | ||||
|                 // TODO: Change the runners length on config reloads | ||||
|                 // TODO: Update this on config reloads. Or possibly on world restarts? | ||||
|                 runners = new TaskRunner[ComputerCraft.computerThreads]; | ||||
| 
 | ||||
|                 // latency and minPeriod are scaled by 1 + floor(log2(threads)). We can afford to execute tasks for | ||||
| @@ -227,9 +237,14 @@ public final class ComputerThread | ||||
| 
 | ||||
|             executor.virtualRuntime = Math.max( newRuntime, executor.virtualRuntime ); | ||||
| 
 | ||||
|             boolean wasBusy = isBusy(); | ||||
|             // Add to the queue, and signal the workers. | ||||
|             computerQueue.add( executor ); | ||||
|             hasWork.signal(); | ||||
| 
 | ||||
|             // If we've transitioned into a busy state, notify the monitor. This will cause it to sleep for scaledPeriod | ||||
|             // instead of the longer wakeup duration. | ||||
|             if( !wasBusy && isBusy() ) monitorWakeup.signal(); | ||||
|         } | ||||
|         finally | ||||
|         { | ||||
| @@ -346,6 +361,17 @@ public final class ComputerThread | ||||
|         return !computerQueue.isEmpty(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if we have more work queued than we have capacity for. Effectively a more fine-grained version of | ||||
|      * {@link #hasPendingWork()}. | ||||
|      * | ||||
|      * @return If the computer threads are busy. | ||||
|      */ | ||||
|     private static boolean isBusy() | ||||
|     { | ||||
|         return computerQueue.size() > idleWorkers.get(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Observes all currently active {@link TaskRunner}s and terminates their tasks once they have exceeded the hard | ||||
|      * abort limit. | ||||
| @@ -356,16 +382,36 @@ public final class ComputerThread | ||||
|     { | ||||
|         @Override | ||||
|         public void run() | ||||
|         { | ||||
|             try | ||||
|         { | ||||
|             while( true ) | ||||
|             { | ||||
|                     Thread.sleep( MONITOR_WAKEUP ); | ||||
| 
 | ||||
|                     TaskRunner[] currentRunners = ComputerThread.runners; | ||||
|                     if( currentRunners != null ) | ||||
|                 computerLock.lock(); | ||||
|                 try | ||||
|                 { | ||||
|                     // If we've got more work than we have capacity for it, then we'll need to pause a task soon, so | ||||
|                     // sleep for a single pause duration. Otherwise we only need to wake up to set the soft/hard abort | ||||
|                     // flags, which are far less granular. | ||||
|                     monitorWakeup.awaitNanos( isBusy() ? scaledPeriod() : MONITOR_WAKEUP ); | ||||
|                 } | ||||
|                 catch( InterruptedException e ) | ||||
|                 { | ||||
|                     ComputerCraft.log.error( "Monitor thread interrupted. Computers may behave very badly!", e ); | ||||
|                     break; | ||||
|                 } | ||||
|                 finally | ||||
|                 { | ||||
|                     computerLock.unlock(); | ||||
|                 } | ||||
| 
 | ||||
|                 checkRunners(); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         private static void checkRunners() | ||||
|         { | ||||
|             TaskRunner[] currentRunners = ComputerThread.runners; | ||||
|             if( currentRunners == null ) return; | ||||
| 
 | ||||
|             for( int i = 0; i < currentRunners.length; i++ ) | ||||
|             { | ||||
|                 TaskRunner runner = currentRunners[i]; | ||||
| @@ -385,6 +431,9 @@ public final class ComputerThread | ||||
|                 ComputerExecutor executor = runner.currentExecutor.get(); | ||||
|                 if( executor == null ) continue; | ||||
| 
 | ||||
|                 // Refresh the timeout state. Will set the pause/soft timeout flags as appropriate. | ||||
|                 executor.timeout.refresh(); | ||||
| 
 | ||||
|                 // If we're still within normal execution times (TIMEOUT) or soft abort (ABORT_TIMEOUT), | ||||
|                 // then we can let the Lua machine do its work. | ||||
|                 long afterStart = executor.timeout.nanoCumulative(); | ||||
| @@ -399,7 +448,7 @@ public final class ComputerThread | ||||
|                 { | ||||
|                     // If we've hard aborted and interrupted, and we're still not dead, then mark the runner | ||||
|                     // as dead, finish off the task, and spawn a new runner. | ||||
|                                 timeoutTask( executor, runner.owner, afterStart ); | ||||
|                     runner.reportTimeout( executor, afterStart ); | ||||
|                     runner.running = false; | ||||
|                     runner.owner.interrupt(); | ||||
| 
 | ||||
| @@ -418,18 +467,12 @@ public final class ComputerThread | ||||
|                 { | ||||
|                     // If we've hard aborted but we're still not dead, dump the stack trace and interrupt | ||||
|                     // the task. | ||||
|                                 timeoutTask( executor, runner.owner, afterStart ); | ||||
|                     runner.reportTimeout( executor, afterStart ); | ||||
|                     runner.owner.interrupt(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|             } | ||||
|             catch( InterruptedException ignored ) | ||||
|             { | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Pulls tasks from the {@link #computerQueue} queue and runs them. | ||||
| @@ -441,6 +484,7 @@ public final class ComputerThread | ||||
|     private static final class TaskRunner implements Runnable | ||||
|     { | ||||
|         Thread owner; | ||||
|         long lastReport = Long.MIN_VALUE; | ||||
|         volatile boolean running = true; | ||||
| 
 | ||||
|         final AtomicReference<ComputerExecutor> currentExecutor = new AtomicReference<>(); | ||||
| @@ -460,6 +504,7 @@ public final class ComputerThread | ||||
|                     computerLock.lockInterruptibly(); | ||||
|                     try | ||||
|                     { | ||||
|                         idleWorkers.incrementAndGet(); | ||||
|                         while( computerQueue.isEmpty() ) hasWork.await(); | ||||
|                         executor = computerQueue.pollFirst(); | ||||
|                         assert executor != null : "hasWork should ensure we never receive null work"; | ||||
| @@ -467,6 +512,7 @@ public final class ComputerThread | ||||
|                     finally | ||||
|                     { | ||||
|                         computerLock.unlock(); | ||||
|                         idleWorkers.decrementAndGet(); | ||||
|                     } | ||||
|                 } | ||||
|                 catch( InterruptedException ignored ) | ||||
| @@ -516,23 +562,27 @@ public final class ComputerThread | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private static void timeoutTask( ComputerExecutor executor, Thread thread, long time ) | ||||
|         private void reportTimeout( ComputerExecutor executor, long time ) | ||||
|         { | ||||
|             if( !ComputerCraft.logComputerErrors ) return; | ||||
| 
 | ||||
|             // Attempt to debounce stack trace reporting, limiting ourselves to one every second. | ||||
|             long now = System.nanoTime(); | ||||
|             if( lastReport != Long.MIN_VALUE && now - lastReport - REPORT_DEBOUNCE <= 0 ) return; | ||||
|             lastReport = now; | ||||
| 
 | ||||
|             StringBuilder builder = new StringBuilder() | ||||
|                 .append( "Terminating computer #" ).append( executor.getComputer().getID() ) | ||||
|                 .append( " due to timeout (running for " ).append( time * 1e-9 ) | ||||
|                 .append( " seconds). This is NOT a bug, but may mean a computer is misbehaving. " ) | ||||
|             .append( thread.getName() ) | ||||
|                 .append( owner.getName() ) | ||||
|                 .append( " is currently " ) | ||||
|             .append( thread.getState() ); | ||||
|         Object blocking = LockSupport.getBlocker( thread ); | ||||
|                 .append( owner.getState() ); | ||||
|             Object blocking = LockSupport.getBlocker( owner ); | ||||
|             if( blocking != null ) builder.append( "\n  on " ).append( blocking ); | ||||
| 
 | ||||
|         for( StackTraceElement element : thread.getStackTrace() ) | ||||
|             for( StackTraceElement element : owner.getStackTrace() ) | ||||
|             { | ||||
|                 builder.append( "\n  at " ).append( element ); | ||||
|             } | ||||
| @@ -540,3 +590,4 @@ public final class ComputerThread | ||||
|             ComputerCraft.log.warn( builder.toString() ); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -86,7 +86,7 @@ public final class TimeoutState | ||||
|     /** | ||||
|      * Recompute the {@link #isSoftAborted()} and {@link #isPaused()} flags. | ||||
|      */ | ||||
|     public void refresh() | ||||
|     public synchronized void refresh() | ||||
|     { | ||||
|         // Important: The weird arithmetic here is important, as nanoTime may return negative values, and so we | ||||
|         // need to handle overflow. | ||||
| @@ -153,7 +153,7 @@ public final class TimeoutState | ||||
|      * | ||||
|      * @see #nanoCumulative() | ||||
|      */ | ||||
|     void pauseTimer() | ||||
|     synchronized void pauseTimer() | ||||
|     { | ||||
|         // We set the cumulative time to difference between current time and "nominal start time". | ||||
|         cumulativeElapsed = System.nanoTime() - cumulativeStart; | ||||
| @@ -163,7 +163,7 @@ public final class TimeoutState | ||||
|     /** | ||||
|      * Resets the cumulative time and resets the abort flags. | ||||
|      */ | ||||
|     void stopTimer() | ||||
|     synchronized void stopTimer() | ||||
|     { | ||||
|         cumulativeElapsed = 0; | ||||
|         paused = softAbort = hardAbort = false; | ||||
|   | ||||
| @@ -41,7 +41,7 @@ class BasicFunction extends VarArgFunction | ||||
|     @Override | ||||
|     public Varargs invoke( LuaState luaState, Varargs args ) throws LuaError | ||||
|     { | ||||
|         IArguments arguments = CobaltLuaMachine.toArguments( args ); | ||||
|         IArguments arguments = VarargArguments.of( args ); | ||||
|         MethodResult results; | ||||
|         try | ||||
|         { | ||||
|   | ||||
| @@ -424,11 +424,6 @@ public class CobaltLuaMachine implements ILuaMachine | ||||
|         return objects; | ||||
|     } | ||||
| 
 | ||||
|     static IArguments toArguments( Varargs values ) | ||||
|     { | ||||
|         return values == Constants.NONE ? VarargArguments.EMPTY : new VarargArguments( values ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * A {@link DebugHandler} which observes the {@link TimeoutState} and responds accordingly. | ||||
|      */ | ||||
| @@ -457,24 +452,9 @@ public class CobaltLuaMachine implements ILuaMachine | ||||
|             // We check our current pause/abort state every 128 instructions. | ||||
|             if( (count = (count + 1) & 127) == 0 ) | ||||
|             { | ||||
|                 // If we've been hard aborted or closed then abort. | ||||
|                 if( timeout.isHardAborted() || state == null ) throw HardAbortError.INSTANCE; | ||||
| 
 | ||||
|                 timeout.refresh(); | ||||
|                 if( timeout.isPaused() ) | ||||
|                 { | ||||
|                     // Preserve the current state | ||||
|                     isPaused = true; | ||||
|                     oldInHook = ds.inhook; | ||||
|                     oldFlags = di.flags; | ||||
| 
 | ||||
|                     // Suspend the state. This will probably throw, but we need to handle the case where it won't. | ||||
|                     di.flags |= FLAG_HOOKYIELD | FLAG_HOOKED; | ||||
|                     LuaThread.suspend( ds.getLuaState() ); | ||||
|                     resetPaused( ds, di ); | ||||
|                 } | ||||
| 
 | ||||
|                 handleSoftAbort(); | ||||
|                 if( timeout.isPaused() ) handlePause( ds, di ); | ||||
|                 if( timeout.isSoftAborted() ) handleSoftAbort(); | ||||
|             } | ||||
| 
 | ||||
|             super.onInstruction( ds, di, pc ); | ||||
| @@ -483,13 +463,10 @@ public class CobaltLuaMachine implements ILuaMachine | ||||
|         @Override | ||||
|         public void poll() throws LuaError | ||||
|         { | ||||
|             // If we've been hard aborted or closed then abort. | ||||
|             LuaState state = CobaltLuaMachine.this.state; | ||||
|             if( timeout.isHardAborted() || state == null ) throw HardAbortError.INSTANCE; | ||||
| 
 | ||||
|             timeout.refresh(); | ||||
|             if( timeout.isPaused() ) LuaThread.suspendBlocking( state ); | ||||
|             handleSoftAbort(); | ||||
|             if( timeout.isSoftAborted() ) handleSoftAbort(); | ||||
|         } | ||||
| 
 | ||||
|         private void resetPaused( DebugState ds, DebugFrame di ) | ||||
| @@ -503,11 +480,24 @@ public class CobaltLuaMachine implements ILuaMachine | ||||
|         private void handleSoftAbort() throws LuaError | ||||
|         { | ||||
|             // If we already thrown our soft abort error then don't do it again. | ||||
|             if( !timeout.isSoftAborted() || thrownSoftAbort ) return; | ||||
|             if( thrownSoftAbort ) return; | ||||
| 
 | ||||
|             thrownSoftAbort = true; | ||||
|             throw new LuaError( TimeoutState.ABORT_MESSAGE ); | ||||
|         } | ||||
| 
 | ||||
|         private void handlePause( DebugState ds, DebugFrame di ) throws LuaError, UnwindThrowable | ||||
|         { | ||||
|             // Preserve the current state | ||||
|             isPaused = true; | ||||
|             oldInHook = ds.inhook; | ||||
|             oldFlags = di.flags; | ||||
| 
 | ||||
|             // Suspend the state. This will probably throw, but we need to handle the case where it won't. | ||||
|             di.flags |= FLAG_HOOKYIELD | FLAG_HOOKED; | ||||
|             LuaThread.suspend( ds.getLuaState() ); | ||||
|             resetPaused( ds, di ); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private static final class HardAbortError extends Error | ||||
|   | ||||
| @@ -7,6 +7,8 @@ package dan200.computercraft.core.lua; | ||||
| 
 | ||||
| import dan200.computercraft.api.lua.IDynamicLuaObject; | ||||
| import dan200.computercraft.api.lua.ILuaAPI; | ||||
| import dan200.computercraft.core.computer.Computer; | ||||
| import dan200.computercraft.core.computer.TimeoutState; | ||||
| 
 | ||||
| import javax.annotation.Nonnull; | ||||
| import javax.annotation.Nullable; | ||||
| @@ -63,4 +65,9 @@ public interface ILuaMachine | ||||
|      * Close the Lua machine, aborting any running functions and deleting the internal state. | ||||
|      */ | ||||
|     void close(); | ||||
| 
 | ||||
|     interface Factory | ||||
|     { | ||||
|         ILuaMachine create( Computer computer, TimeoutState timeout ); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -51,7 +51,7 @@ class ResultInterpreterFunction extends ResumableVarArgFunction<ResultInterprete | ||||
|     @Override | ||||
|     protected Varargs invoke( LuaState state, DebugFrame debugFrame, Varargs args ) throws LuaError, UnwindThrowable | ||||
|     { | ||||
|         IArguments arguments = CobaltLuaMachine.toArguments( args ); | ||||
|         IArguments arguments = VarargArguments.of( args ); | ||||
|         MethodResult results; | ||||
|         try | ||||
|         { | ||||
|   | ||||
| @@ -15,19 +15,24 @@ import javax.annotation.Nullable; | ||||
| import java.nio.ByteBuffer; | ||||
| import java.util.Optional; | ||||
| 
 | ||||
| class VarargArguments implements IArguments | ||||
| final class VarargArguments implements IArguments | ||||
| { | ||||
|     static final IArguments EMPTY = new VarargArguments( Constants.NONE ); | ||||
|     private static final IArguments EMPTY = new VarargArguments( Constants.NONE ); | ||||
| 
 | ||||
|     boolean released; | ||||
|     private final Varargs varargs; | ||||
|     private Object[] cache; | ||||
| 
 | ||||
|     VarargArguments( Varargs varargs ) | ||||
|     private VarargArguments( Varargs varargs ) | ||||
|     { | ||||
|         this.varargs = varargs; | ||||
|     } | ||||
| 
 | ||||
|     static IArguments of( Varargs values ) | ||||
|     { | ||||
|         return values == Constants.NONE ? EMPTY : new VarargArguments( values ); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public int count() | ||||
|     { | ||||
|   | ||||
| @@ -18,7 +18,7 @@ import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.Optional; | ||||
| 
 | ||||
| import static dan200.computercraft.ContramapMatcher.contramap; | ||||
| import static dan200.computercraft.support.ContramapMatcher.contramap; | ||||
| import static org.hamcrest.MatcherAssert.assertThat; | ||||
| import static org.hamcrest.Matchers.*; | ||||
| import static org.junit.jupiter.api.Assertions.assertThrows; | ||||
|   | ||||
| @@ -0,0 +1,98 @@ | ||||
| /* | ||||
|  * 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; | ||||
| 
 | ||||
| import dan200.computercraft.ComputerCraft; | ||||
| import dan200.computercraft.core.lua.MachineResult; | ||||
| import dan200.computercraft.support.ConcurrentHelpers; | ||||
| import dan200.computercraft.support.IsolatedRunner; | ||||
| import org.junit.jupiter.api.Test; | ||||
| import org.junit.jupiter.api.Timeout; | ||||
| import org.junit.jupiter.api.extension.ExtendWith; | ||||
| import org.junit.jupiter.api.parallel.Execution; | ||||
| import org.junit.jupiter.api.parallel.ExecutionMode; | ||||
| 
 | ||||
| import java.util.concurrent.TimeUnit; | ||||
| 
 | ||||
| import static org.hamcrest.MatcherAssert.assertThat; | ||||
| import static org.hamcrest.Matchers.closeTo; | ||||
| import static org.junit.jupiter.api.Assertions.*; | ||||
| 
 | ||||
| @Timeout( value = 15 ) | ||||
| @ExtendWith( IsolatedRunner.class ) | ||||
| @Execution( ExecutionMode.CONCURRENT ) | ||||
| public class ComputerThreadTest | ||||
| { | ||||
|     @Test | ||||
|     public void testSoftAbort() throws Exception | ||||
|     { | ||||
|         Computer computer = FakeComputerManager.create(); | ||||
|         FakeComputerManager.enqueue( computer, timeout -> { | ||||
|             assertFalse( timeout.isSoftAborted(), "Should not start soft-aborted" ); | ||||
| 
 | ||||
|             long delay = ConcurrentHelpers.waitUntil( timeout::isSoftAborted ); | ||||
|             assertThat( "Should be soft aborted", delay * 1e-9, closeTo( 7, 0.5 ) ); | ||||
|             ComputerCraft.log.info( "Slept for {}", delay ); | ||||
| 
 | ||||
|             computer.shutdown(); | ||||
|             return MachineResult.OK; | ||||
|         } ); | ||||
| 
 | ||||
|         FakeComputerManager.startAndWait( computer ); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     public void testHardAbort() throws Exception | ||||
|     { | ||||
|         Computer computer = FakeComputerManager.create(); | ||||
|         FakeComputerManager.enqueue( computer, timeout -> { | ||||
|             assertFalse( timeout.isHardAborted(), "Should not start soft-aborted" ); | ||||
| 
 | ||||
|             assertThrows( InterruptedException.class, () -> Thread.sleep( 11_000 ), "Sleep should be hard aborted" ); | ||||
|             assertTrue( timeout.isHardAborted(), "Thread should be hard aborted" ); | ||||
| 
 | ||||
|             computer.shutdown(); | ||||
|             return MachineResult.OK; | ||||
|         } ); | ||||
| 
 | ||||
|         FakeComputerManager.startAndWait( computer ); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     public void testNoPauseIfNoOtherMachines() throws Exception | ||||
|     { | ||||
|         Computer computer = FakeComputerManager.create(); | ||||
|         FakeComputerManager.enqueue( computer, timeout -> { | ||||
|             boolean didPause = ConcurrentHelpers.waitUntil( timeout::isPaused, 5, TimeUnit.SECONDS ); | ||||
|             assertFalse( didPause, "Machine shouldn't have paused within 5s" ); | ||||
| 
 | ||||
|             computer.shutdown(); | ||||
|             return MachineResult.OK; | ||||
|         } ); | ||||
| 
 | ||||
|         FakeComputerManager.startAndWait( computer ); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     public void testPauseIfSomeOtherMachine() throws Exception | ||||
|     { | ||||
|         Computer computer = FakeComputerManager.create(); | ||||
|         FakeComputerManager.enqueue( computer, timeout -> { | ||||
|             long budget = ComputerThread.scaledPeriod(); | ||||
|             assertEquals( budget, TimeUnit.MILLISECONDS.toNanos( 25 ), "Budget should be 25ms" ); | ||||
| 
 | ||||
|             long delay = ConcurrentHelpers.waitUntil( timeout::isPaused ); | ||||
|             assertThat( "Paused within 25ms", delay * 1e-9, closeTo( 0.03, 0.015 ) ); | ||||
| 
 | ||||
|             computer.shutdown(); | ||||
|             return MachineResult.OK; | ||||
|         } ); | ||||
| 
 | ||||
|         FakeComputerManager.createLoopingComputer(); | ||||
| 
 | ||||
|         FakeComputerManager.startAndWait( computer ); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,219 @@ | ||||
| /* | ||||
|  * 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; | ||||
| 
 | ||||
| import dan200.computercraft.api.lua.ILuaAPI; | ||||
| import dan200.computercraft.core.lua.ILuaMachine; | ||||
| import dan200.computercraft.core.lua.MachineResult; | ||||
| import dan200.computercraft.core.terminal.Terminal; | ||||
| import dan200.computercraft.support.IsolatedRunner; | ||||
| import org.jetbrains.annotations.Nullable; | ||||
| 
 | ||||
| import javax.annotation.Nonnull; | ||||
| import java.io.InputStream; | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
| import java.util.Queue; | ||||
| import java.util.concurrent.ConcurrentLinkedQueue; | ||||
| import java.util.concurrent.TimeUnit; | ||||
| import java.util.concurrent.locks.Condition; | ||||
| import java.util.concurrent.locks.Lock; | ||||
| import java.util.concurrent.locks.ReentrantLock; | ||||
| 
 | ||||
| /** | ||||
|  * Creates "fake" computers, which just run user-defined tasks rather than Lua code. | ||||
|  * | ||||
|  * Note, this will clobber some parts of the global state. It's recommended you use this inside an {@link IsolatedRunner}. | ||||
|  */ | ||||
| public class FakeComputerManager | ||||
| { | ||||
|     interface Task | ||||
|     { | ||||
|         MachineResult run( TimeoutState state ) throws Exception; | ||||
|     } | ||||
| 
 | ||||
|     private static final Map<Computer, Queue<Task>> machines = new HashMap<>(); | ||||
| 
 | ||||
|     private static final Lock errorLock = new ReentrantLock(); | ||||
|     private static final Condition hasError = errorLock.newCondition(); | ||||
|     private static volatile Throwable error; | ||||
| 
 | ||||
|     static | ||||
|     { | ||||
|         ComputerExecutor.luaFactory = ( computer, timeout ) -> new DummyLuaMachine( timeout, machines.get( computer ) ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Create a new computer which pulls from our task queue. | ||||
|      * | ||||
|      * @return The computer. This will not be started yet, you must call {@link Computer#turnOn()} and | ||||
|      * {@link Computer#tick()} to do so. | ||||
|      */ | ||||
|     @Nonnull | ||||
|     public static Computer create() | ||||
|     { | ||||
|         Computer computer = new Computer( new BasicEnvironment(), new Terminal( 51, 19 ), 0 ); | ||||
|         machines.put( computer, new ConcurrentLinkedQueue<>() ); | ||||
|         return computer; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Create and start a new computer which loops forever. | ||||
|      */ | ||||
|     public static void createLoopingComputer() | ||||
|     { | ||||
|         Computer computer = create(); | ||||
|         enqueueForever( computer, t -> { | ||||
|             Thread.sleep( 100 ); | ||||
|             return 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. | ||||
|      */ | ||||
|     public static void enqueue( @Nonnull Computer computer, @Nonnull Task task ) | ||||
|     { | ||||
|         machines.get( 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 static void enqueueForever( @Nonnull Computer computer, @Nonnull Task task ) | ||||
|     { | ||||
|         machines.get( computer ).offer( t -> { | ||||
|             MachineResult result = task.run( t ); | ||||
| 
 | ||||
|             enqueueForever( computer, task ); | ||||
|             computer.queueEvent( "some_event", null ); | ||||
|             return 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. | ||||
|      */ | ||||
|     public static void sleep( long delay, TimeUnit unit ) throws Exception | ||||
|     { | ||||
|         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. | ||||
|      */ | ||||
|     public static void startAndWait( Computer computer ) throws Exception | ||||
|     { | ||||
|         computer.turnOn(); | ||||
|         computer.tick(); | ||||
| 
 | ||||
|         do | ||||
|         { | ||||
|             sleep( 100, TimeUnit.MILLISECONDS ); | ||||
|         } while( ComputerThread.hasPendingWork() || computer.isOn() ); | ||||
| 
 | ||||
|         rethrowIfNeeded(); | ||||
|     } | ||||
| 
 | ||||
|     private static void rethrowIfNeeded() throws Exception | ||||
|     { | ||||
|         if( error == null ) return; | ||||
|         if( error instanceof Exception ) throw (Exception) error; | ||||
|         if( error instanceof Error ) throw (Error) error; | ||||
|         rethrow( error ); | ||||
|     } | ||||
| 
 | ||||
|     @SuppressWarnings( "unchecked" ) | ||||
|     private static <T extends Throwable> void rethrow( Throwable e ) throws T | ||||
|     { | ||||
|         throw (T) e; | ||||
|     } | ||||
| 
 | ||||
|     private static class DummyLuaMachine implements ILuaMachine | ||||
|     { | ||||
|         private final TimeoutState state; | ||||
|         private final Queue<Task> handleEvent; | ||||
| 
 | ||||
|         DummyLuaMachine( TimeoutState state, Queue<Task> handleEvent ) | ||||
|         { | ||||
|             this.state = state; | ||||
|             this.handleEvent = handleEvent; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void addAPI( @Nonnull ILuaAPI api ) | ||||
|         { | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public MachineResult loadBios( @Nonnull InputStream bios ) | ||||
|         { | ||||
|             return MachineResult.OK; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public MachineResult handleEvent( @Nullable String eventName, @Nullable Object[] arguments ) | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 return handleEvent.remove().run( state ); | ||||
|             } | ||||
|             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) ) rethrow( e ); | ||||
|                 return MachineResult.error( e.getMessage() ); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void close() | ||||
|         { | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -5,7 +5,7 @@ | ||||
|  */ | ||||
| package dan200.computercraft.core.terminal; | ||||
| 
 | ||||
| import dan200.computercraft.ContramapMatcher; | ||||
| import dan200.computercraft.support.ContramapMatcher; | ||||
| import org.hamcrest.Matcher; | ||||
| import org.hamcrest.Matchers; | ||||
| 
 | ||||
| @@ -36,11 +36,11 @@ public class TerminalMatchers | ||||
| 
 | ||||
|     public static Matcher<Terminal> linesMatchWith( String kind, LineProvider getLine, Matcher<String>[] lines ) | ||||
|     { | ||||
|         return new ContramapMatcher<>( kind, terminal -> { | ||||
|         return ContramapMatcher.contramap( Matchers.array( lines ), kind, terminal -> { | ||||
|             String[] termLines = new String[terminal.getHeight()]; | ||||
|             for( int i = 0; i < termLines.length; i++ ) termLines[i] = getLine.getLine( terminal, i ).toString(); | ||||
|             return termLines; | ||||
|         }, Matchers.array( lines ) ); | ||||
|         } ); | ||||
|     } | ||||
| 
 | ||||
|     @FunctionalInterface | ||||
|   | ||||
| @@ -7,7 +7,7 @@ package dan200.computercraft.core.terminal; | ||||
| 
 | ||||
| import dan200.computercraft.api.lua.LuaValues; | ||||
| import dan200.computercraft.shared.util.Colour; | ||||
| import dan200.computercraft.utils.CallCounter; | ||||
| import dan200.computercraft.support.CallCounter; | ||||
| import io.netty.buffer.Unpooled; | ||||
| import net.minecraft.nbt.CompoundNBT; | ||||
| import net.minecraft.network.PacketBuffer; | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
|  * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. | ||||
|  * Send enquiries to dratcliffe@gmail.com | ||||
|  */ | ||||
| package dan200.computercraft.utils; | ||||
| package dan200.computercraft.support; | ||||
| 
 | ||||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||||
| 
 | ||||
| @@ -0,0 +1,56 @@ | ||||
| /* | ||||
|  * 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.support; | ||||
| 
 | ||||
| import java.util.concurrent.TimeUnit; | ||||
| import java.util.concurrent.locks.LockSupport; | ||||
| import java.util.function.BooleanSupplier; | ||||
| 
 | ||||
| /** | ||||
|  * Utilities for working with concurrent systems. | ||||
|  */ | ||||
| public class ConcurrentHelpers | ||||
| { | ||||
|     private static final long DELAY = TimeUnit.MILLISECONDS.toNanos( 2 ); | ||||
| 
 | ||||
|     /** | ||||
|      * Wait until a condition is true, checking the condition every 2ms. | ||||
|      * | ||||
|      * @param isTrue The condition to check | ||||
|      * @return How long we waited for. | ||||
|      */ | ||||
|     public static long waitUntil( BooleanSupplier isTrue ) | ||||
|     { | ||||
|         long start = System.nanoTime(); | ||||
|         while( true ) | ||||
|         { | ||||
|             if( isTrue.getAsBoolean() ) return System.nanoTime() - start; | ||||
|             LockSupport.parkNanos( DELAY ); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Wait until a condition is true or a timeout is elapsed, checking the condition every 2ms. | ||||
|      * | ||||
|      * @param isTrue  The condition to check | ||||
|      * @param timeout The delay after which we will timeout. | ||||
|      * @param unit    The time unit the duration is measured in. | ||||
|      * @return {@literal true} if the condition was met, {@literal false} if we timed out instead. | ||||
|      */ | ||||
|     public static boolean waitUntil( BooleanSupplier isTrue, long timeout, TimeUnit unit ) | ||||
|     { | ||||
|         long start = System.nanoTime(); | ||||
|         long timeoutNs = unit.toNanos( timeout ); | ||||
|         while( true ) | ||||
|         { | ||||
|             long time = System.nanoTime() - start; | ||||
|             if( isTrue.getAsBoolean() ) return true; | ||||
|             if( time > timeoutNs ) return false; | ||||
| 
 | ||||
|             LockSupport.parkNanos( DELAY ); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -3,42 +3,34 @@ | ||||
|  * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. | ||||
|  * Send enquiries to dratcliffe@gmail.com | ||||
|  */ | ||||
| package dan200.computercraft; | ||||
| package dan200.computercraft.support; | ||||
| 
 | ||||
| import org.hamcrest.Description; | ||||
| import org.hamcrest.FeatureMatcher; | ||||
| import org.hamcrest.Matcher; | ||||
| import org.hamcrest.TypeSafeDiagnosingMatcher; | ||||
| 
 | ||||
| import java.util.function.Function; | ||||
| 
 | ||||
| public class ContramapMatcher<T, U> extends TypeSafeDiagnosingMatcher<T> | ||||
| /** | ||||
|  * Given some function from {@code T} to {@code U}, converts a {@code Matcher<U>} to {@code Matcher<T>}. This is useful | ||||
|  * when you want to match on a particular field (or some other projection) as part of a larger matcher. | ||||
|  * | ||||
|  * @param <T> The type of the object to be matched. | ||||
|  * @param <U> The type of the projection/field to be matched. | ||||
|  */ | ||||
| public final class ContramapMatcher<T, U> extends FeatureMatcher<T, U> | ||||
| { | ||||
|     private final String desc; | ||||
|     private final Function<T, U> convert; | ||||
|     private final Matcher<U> matcher; | ||||
| 
 | ||||
|     public ContramapMatcher( String desc, Function<T, U> convert, Matcher<U> matcher ) | ||||
|     { | ||||
|         this.desc = desc; | ||||
|         super( matcher, desc, desc ); | ||||
|         this.convert = convert; | ||||
|         this.matcher = matcher; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected boolean matchesSafely( T item, Description mismatchDescription ) | ||||
|     protected U featureValueOf( T actual ) | ||||
|     { | ||||
|         U converted = convert.apply( item ); | ||||
|         if( matcher.matches( converted ) ) return true; | ||||
| 
 | ||||
|         mismatchDescription.appendText( desc ).appendText( " " ); | ||||
|         matcher.describeMismatch( converted, mismatchDescription ); | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void describeTo( Description description ) | ||||
|     { | ||||
|         description.appendText( desc ).appendText( " " ).appendDescriptionOf( matcher ); | ||||
|         return convert.apply( actual ); | ||||
|     } | ||||
| 
 | ||||
|     public static <T, U> Matcher<T> contramap( Matcher<U> matcher, String desc, Function<T, U> convert ) | ||||
							
								
								
									
										110
									
								
								src/test/java/dan200/computercraft/support/IsolatedRunner.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								src/test/java/dan200/computercraft/support/IsolatedRunner.java
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | ||||
| /* | ||||
|  * 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.support; | ||||
| 
 | ||||
| import com.google.common.io.ByteStreams; | ||||
| import net.minecraftforge.fml.unsafe.UnsafeHacks; | ||||
| import org.junit.jupiter.api.extension.*; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.lang.reflect.Field; | ||||
| import java.lang.reflect.InvocationTargetException; | ||||
| import java.lang.reflect.Method; | ||||
| import java.security.CodeSource; | ||||
| import java.security.SecureClassLoader; | ||||
| 
 | ||||
| /** | ||||
|  * Runs a test method in an entirely isolated {@link ClassLoader}, so you can mess around with as much of | ||||
|  * {@link dan200.computercraft} as you like. | ||||
|  * | ||||
|  * This <strong>IS NOT</strong> a good idea, but helps us run some tests in parallel while having lots of (terrible) | ||||
|  * global state. | ||||
|  */ | ||||
| public class IsolatedRunner implements InvocationInterceptor, BeforeEachCallback, AfterEachCallback | ||||
| { | ||||
|     private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace.create( new Object() ); | ||||
| 
 | ||||
|     @Override | ||||
|     public void beforeEach( ExtensionContext context ) throws Exception | ||||
|     { | ||||
|         ClassLoader loader = context.getStore( NAMESPACE ).getOrComputeIfAbsent( IsolatedClassLoader.class ); | ||||
| 
 | ||||
|         // Rename the global thread group to something more obvious. | ||||
|         ThreadGroup group = (ThreadGroup) loader.loadClass( "dan200.computercraft.shared.util.ThreadUtils" ).getMethod( "group" ).invoke( null ); | ||||
|         Field field = ThreadGroup.class.getDeclaredField( "name" ); | ||||
|         UnsafeHacks.setField( field, group, "<" + context.getDisplayName() + ">" ); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void afterEach( ExtensionContext context ) throws Exception | ||||
|     { | ||||
|         ClassLoader loader = context.getStore( NAMESPACE ).get( IsolatedClassLoader.class, IsolatedClassLoader.class ); | ||||
|         loader.loadClass( "dan200.computercraft.core.computer.ComputerThread" ) | ||||
|             .getDeclaredMethod( "stop" ) | ||||
|             .invoke( null ); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     @Override | ||||
|     public void interceptTestMethod( Invocation<Void> invocation, ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext ) throws Throwable | ||||
|     { | ||||
|         invocation.skip(); | ||||
| 
 | ||||
|         ClassLoader loader = extensionContext.getStore( NAMESPACE ).get( IsolatedClassLoader.class, IsolatedClassLoader.class ); | ||||
|         Method method = invocationContext.getExecutable(); | ||||
| 
 | ||||
|         Class<?> ourClass = loader.loadClass( method.getDeclaringClass().getName() ); | ||||
|         Method ourMethod = ourClass.getDeclaredMethod( method.getName(), method.getParameterTypes() ); | ||||
| 
 | ||||
|         try | ||||
|         { | ||||
|             ourMethod.invoke( ourClass.getConstructor().newInstance(), invocationContext.getArguments().toArray() ); | ||||
|         } | ||||
|         catch( InvocationTargetException e ) | ||||
|         { | ||||
|             throw e.getTargetException(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private static class IsolatedClassLoader extends SecureClassLoader | ||||
|     { | ||||
|         IsolatedClassLoader() | ||||
|         { | ||||
|             super( IsolatedClassLoader.class.getClassLoader() ); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public Class<?> loadClass( String name, boolean resolve ) throws ClassNotFoundException | ||||
|         { | ||||
|             synchronized( getClassLoadingLock( name ) ) | ||||
|             { | ||||
|                 Class<?> c = findLoadedClass( name ); | ||||
|                 if( c != null ) return c; | ||||
| 
 | ||||
|                 if( name.startsWith( "dan200.computercraft." ) ) | ||||
|                 { | ||||
|                     CodeSource parentSource = getParent().loadClass( name ).getProtectionDomain().getCodeSource(); | ||||
| 
 | ||||
|                     byte[] contents; | ||||
|                     try( InputStream stream = getResourceAsStream( name.replace( '.', '/' ) + ".class" ) ) | ||||
|                     { | ||||
|                         if( stream == null ) throw new ClassNotFoundException( name ); | ||||
|                         contents = ByteStreams.toByteArray( stream ); | ||||
|                     } | ||||
|                     catch( IOException e ) | ||||
|                     { | ||||
|                         throw new ClassNotFoundException( name, e ); | ||||
|                     } | ||||
| 
 | ||||
|                     return defineClass( name, contents, 0, contents.length, parentSource ); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             return super.loadClass( name, resolve ); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										2
									
								
								src/test/resources/junit-platform.properties
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/test/resources/junit-platform.properties
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| junit.jupiter.execution.parallel.enabled=true | ||||
| junit.jupiter.execution.parallel.config.dynamic.factor=4 | ||||
		Reference in New Issue
	
	Block a user
	 Jonathan Coates
					Jonathan Coates