mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-01-28 09:54:47 +00:00
Merge branch 'feature/optimise-timeouts' into mc-1.16.x
This commit is contained in:
commit
ad2d1d6a05
@ -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.
|
||||
@ -357,76 +383,93 @@ public final class ComputerThread
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
while( true )
|
||||
{
|
||||
while( true )
|
||||
computerLock.lock();
|
||||
try
|
||||
{
|
||||
Thread.sleep( MONITOR_WAKEUP );
|
||||
// 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();
|
||||
}
|
||||
|
||||
TaskRunner[] currentRunners = ComputerThread.runners;
|
||||
if( currentRunners != null )
|
||||
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];
|
||||
// If we've no runner, skip.
|
||||
if( runner == null || runner.owner == null || !runner.owner.isAlive() )
|
||||
{
|
||||
if( !running ) continue;
|
||||
|
||||
// Mark the old runner as dead and start a new one.
|
||||
ComputerCraft.log.warn( "Previous runner ({}) has crashed, restarting!",
|
||||
runner != null && runner.owner != null ? runner.owner.getName() : runner );
|
||||
if( runner != null ) runner.running = false;
|
||||
runnerFactory.newThread( runners[i] = new TaskRunner() ).start();
|
||||
}
|
||||
|
||||
// If the runner has no work, skip
|
||||
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();
|
||||
long afterHardAbort = afterStart - TIMEOUT - ABORT_TIMEOUT;
|
||||
if( afterHardAbort < 0 ) continue;
|
||||
|
||||
// Set the hard abort flag.
|
||||
executor.timeout.hardAbort();
|
||||
executor.abort();
|
||||
|
||||
if( afterHardAbort >= ABORT_TIMEOUT * 2 )
|
||||
{
|
||||
// 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.
|
||||
runner.reportTimeout( executor, afterStart );
|
||||
runner.running = false;
|
||||
runner.owner.interrupt();
|
||||
|
||||
ComputerExecutor thisExecutor = runner.currentExecutor.getAndSet( null );
|
||||
if( thisExecutor != null ) afterWork( runner, executor );
|
||||
|
||||
synchronized( threadLock )
|
||||
{
|
||||
for( int i = 0; i < currentRunners.length; i++ )
|
||||
if( running && runners.length > i && runners[i] == runner )
|
||||
{
|
||||
TaskRunner runner = currentRunners[i];
|
||||
// If we've no runner, skip.
|
||||
if( runner == null || runner.owner == null || !runner.owner.isAlive() )
|
||||
{
|
||||
if( !running ) continue;
|
||||
|
||||
// Mark the old runner as dead and start a new one.
|
||||
ComputerCraft.log.warn( "Previous runner ({}) has crashed, restarting!",
|
||||
runner != null && runner.owner != null ? runner.owner.getName() : runner );
|
||||
if( runner != null ) runner.running = false;
|
||||
runnerFactory.newThread( runners[i] = new TaskRunner() ).start();
|
||||
}
|
||||
|
||||
// If the runner has no work, skip
|
||||
ComputerExecutor executor = runner.currentExecutor.get();
|
||||
if( executor == null ) continue;
|
||||
|
||||
// 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();
|
||||
long afterHardAbort = afterStart - TIMEOUT - ABORT_TIMEOUT;
|
||||
if( afterHardAbort < 0 ) continue;
|
||||
|
||||
// Set the hard abort flag.
|
||||
executor.timeout.hardAbort();
|
||||
executor.abort();
|
||||
|
||||
if( afterHardAbort >= ABORT_TIMEOUT * 2 )
|
||||
{
|
||||
// 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.running = false;
|
||||
runner.owner.interrupt();
|
||||
|
||||
ComputerExecutor thisExecutor = runner.currentExecutor.getAndSet( null );
|
||||
if( thisExecutor != null ) afterWork( runner, executor );
|
||||
|
||||
synchronized( threadLock )
|
||||
{
|
||||
if( running && runners.length > i && runners[i] == runner )
|
||||
{
|
||||
runnerFactory.newThread( currentRunners[i] = new TaskRunner() ).start();
|
||||
}
|
||||
}
|
||||
}
|
||||
else if( afterHardAbort >= ABORT_TIMEOUT )
|
||||
{
|
||||
// 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.owner.interrupt();
|
||||
}
|
||||
runnerFactory.newThread( currentRunners[i] = new TaskRunner() ).start();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch( InterruptedException ignored )
|
||||
{
|
||||
else if( afterHardAbort >= ABORT_TIMEOUT )
|
||||
{
|
||||
// If we've hard aborted but we're still not dead, dump the stack trace and interrupt
|
||||
// the task.
|
||||
runner.reportTimeout( executor, afterStart );
|
||||
runner.owner.interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,27 +562,32 @@ public final class ComputerThread
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void timeoutTask( ComputerExecutor executor, Thread thread, long time )
|
||||
{
|
||||
if( !ComputerCraft.logComputerErrors ) return;
|
||||
|
||||
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( " is currently " )
|
||||
.append( thread.getState() );
|
||||
Object blocking = LockSupport.getBlocker( thread );
|
||||
if( blocking != null ) builder.append( "\n on " ).append( blocking );
|
||||
|
||||
for( StackTraceElement element : thread.getStackTrace() )
|
||||
private void reportTimeout( ComputerExecutor executor, long time )
|
||||
{
|
||||
builder.append( "\n at " ).append( element );
|
||||
}
|
||||
if( !ComputerCraft.logComputerErrors ) return;
|
||||
|
||||
ComputerCraft.log.warn( builder.toString() );
|
||||
// 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( owner.getName() )
|
||||
.append( " is currently " )
|
||||
.append( owner.getState() );
|
||||
Object blocking = LockSupport.getBlocker( owner );
|
||||
if( blocking != null ) builder.append( "\n on " ).append( blocking );
|
||||
|
||||
for( StackTraceElement element : owner.getStackTrace() )
|
||||
{
|
||||
builder.append( "\n at " ).append( element );
|
||||
}
|
||||
|
||||
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
|
Loading…
Reference in New Issue
Block a user