1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2024-06-28 08:03:21 +00:00

Rewrite the metrics system

- Remove TrackingField and replace it with a Metric abstract class.
   This has two concrete subclasses - Counter and Event. Events carry an
   additional piece of data each time it is observed, such as HTTP
   response size.

 - Computers now accept a MetricsObserver, which tracks metrics for this
   particular computer. This allows us to decouple Computer classes and
   metrics information. The concrete MetricsObserver class we use within
   Minecraft exposes the ServerComputer directly, so we no longer need to
   do the awkward mapping and lookups!

 - The /computercraft command can now do aggregates (count, avg, max)
   over all Event metrics. This removes the need for special handling of
   computer and server time.

There's also a small number of changes in removing the coupling between
Computer and some of its dependencies (ILuaMachine, MainThreadExecutor).
This makes some future refactorings easier, I promise!
This commit is contained in:
Jonathan Coates 2022-10-22 01:35:04 +01:00
parent cb9731306c
commit 0b26ab366d
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
43 changed files with 1107 additions and 760 deletions

View File

@ -16,7 +16,7 @@
import dan200.computercraft.core.filesystem.FileSystem;
import dan200.computercraft.core.filesystem.FileSystemException;
import dan200.computercraft.core.filesystem.FileSystemWrapper;
import dan200.computercraft.core.tracking.TrackingField;
import dan200.computercraft.core.metrics.Metrics;
import java.io.BufferedReader;
import java.io.BufferedWriter;
@ -108,7 +108,7 @@ public void shutdown()
@LuaFunction
public final String[] list( String path ) throws LuaException
{
environment.addTrackingChange( TrackingField.FS_OPS );
environment.observe( Metrics.FS_OPS );
try
{
return fileSystem.list( path );
@ -276,7 +276,7 @@ public final void makeDir( String path ) throws LuaException
{
try
{
environment.addTrackingChange( TrackingField.FS_OPS );
environment.observe( Metrics.FS_OPS );
fileSystem.makeDir( path );
}
catch( FileSystemException e )
@ -299,7 +299,7 @@ public final void move( String path, String dest ) throws LuaException
{
try
{
environment.addTrackingChange( TrackingField.FS_OPS );
environment.observe( Metrics.FS_OPS );
fileSystem.move( path, dest );
}
catch( FileSystemException e )
@ -322,7 +322,7 @@ public final void copy( String path, String dest ) throws LuaException
{
try
{
environment.addTrackingChange( TrackingField.FS_OPS );
environment.observe( Metrics.FS_OPS );
fileSystem.copy( path, dest );
}
catch( FileSystemException e )
@ -345,7 +345,7 @@ public final void delete( String path ) throws LuaException
{
try
{
environment.addTrackingChange( TrackingField.FS_OPS );
environment.observe( Metrics.FS_OPS );
fileSystem.delete( path );
}
catch( FileSystemException e )
@ -411,7 +411,7 @@ public final void delete( String path ) throws LuaException
@LuaFunction
public final Object[] open( String path, String mode ) throws LuaException
{
environment.addTrackingChange( TrackingField.FS_OPS );
environment.observe( Metrics.FS_OPS );
try
{
switch( mode )
@ -532,7 +532,7 @@ public final String[] find( String path ) throws LuaException
{
try
{
environment.addTrackingChange( TrackingField.FS_OPS );
environment.observe( Metrics.FS_OPS );
return fileSystem.find( path );
}
catch( FileSystemException e )

View File

@ -11,8 +11,8 @@
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.core.computer.GlobalEnvironment;
import dan200.computercraft.core.filesystem.FileSystem;
import dan200.computercraft.core.metrics.Metric;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.tracking.TrackingField;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@ -75,10 +75,7 @@ interface IPeripheralChangeListener
void cancelTimer( int id );
void addTrackingChange( @Nonnull TrackingField field, long change );
void observe( @Nonnull Metric.Event event, long change );
default void addTrackingChange( @Nonnull TrackingField field )
{
addTrackingChange( field, 1 );
}
void observe( @Nonnull Metric.Counter counter );
}

View File

@ -16,7 +16,7 @@
import dan200.computercraft.core.asm.NamedMethod;
import dan200.computercraft.core.asm.PeripheralMethod;
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.core.tracking.TrackingField;
import dan200.computercraft.core.metrics.Metrics;
import dan200.computercraft.shared.util.LuaUtil;
import javax.annotation.Nonnull;
@ -108,7 +108,7 @@ public MethodResult call( ILuaContext context, String methodName, IArguments arg
if( method == null ) throw new LuaException( "No such method " + methodName );
environment.addTrackingChange( TrackingField.PERIPHERAL_OPS );
environment.observe( Metrics.PERIPHERAL_OPS );
return method.apply( peripheral, context, this, arguments );
}

View File

@ -12,7 +12,7 @@
import dan200.computercraft.core.apis.http.Resource;
import dan200.computercraft.core.apis.http.ResourceGroup;
import dan200.computercraft.core.apis.http.options.Options;
import dan200.computercraft.core.tracking.TrackingField;
import dan200.computercraft.core.metrics.Metrics;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
@ -148,8 +148,8 @@ private void doRequest( URI uri, HttpMethod method )
}
// Add request size to the tracker before opening the connection
environment.addTrackingChange( TrackingField.HTTP_REQUESTS, 1 );
environment.addTrackingChange( TrackingField.HTTP_UPLOAD, requestBody );
environment.observe( Metrics.HTTP_REQUESTS );
environment.observe( Metrics.HTTP_UPLOAD, requestBody );
HttpRequestHandler handler = currentRequest = new HttpRequestHandler( this, uri, method, options );
connectFuture = new Bootstrap()

View File

@ -13,7 +13,7 @@
import dan200.computercraft.core.apis.http.HTTPRequestException;
import dan200.computercraft.core.apis.http.NetworkUtils;
import dan200.computercraft.core.apis.http.options.Options;
import dan200.computercraft.core.tracking.TrackingField;
import dan200.computercraft.core.metrics.Metrics;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.CompositeByteBuf;
import io.netty.channel.ChannelHandlerContext;
@ -202,7 +202,7 @@ private void sendResponse()
}
// Fire off a stats event
request.environment().addTrackingChange( TrackingField.HTTP_DOWNLOAD, getHeaderSize( responseHeaders ) + bytes.length );
request.environment().observe( Metrics.HTTP_DOWNLOAD, getHeaderSize( responseHeaders ) + bytes.length );
// Prepare to queue an event
ArrayByteChannel contents = new ArrayByteChannel( bytes );

View File

@ -8,7 +8,7 @@
import com.google.common.base.Objects;
import dan200.computercraft.api.lua.*;
import dan200.computercraft.core.apis.http.options.Options;
import dan200.computercraft.core.tracking.TrackingField;
import dan200.computercraft.core.metrics.Metrics;
import dan200.computercraft.shared.util.StringUtil;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
@ -89,7 +89,7 @@ public final void send( Object message, Optional<Boolean> binary ) throws LuaExc
throw new LuaException( "Message is too large" );
}
websocket.environment().addTrackingChange( TrackingField.WEBSOCKET_OUTGOING, text.length() );
websocket.environment().observe( Metrics.WEBSOCKET_OUTGOING, text.length() );
Channel channel = this.channel;
if( channel != null )

View File

@ -7,7 +7,7 @@
import dan200.computercraft.core.apis.http.NetworkUtils;
import dan200.computercraft.core.apis.http.options.Options;
import dan200.computercraft.core.tracking.TrackingField;
import dan200.computercraft.core.metrics.Metrics;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.FullHttpResponse;
@ -66,14 +66,14 @@ public void channelRead0( ChannelHandlerContext ctx, Object msg )
{
String data = ((TextWebSocketFrame) frame).text();
websocket.environment().addTrackingChange( TrackingField.WEBSOCKET_INCOMING, data.length() );
websocket.environment().observe( Metrics.WEBSOCKET_INCOMING, data.length() );
websocket.environment().queueEvent( MESSAGE_EVENT, websocket.address(), data, false );
}
else if( frame instanceof BinaryWebSocketFrame )
{
byte[] converted = NetworkUtils.toBytes( frame.content() );
websocket.environment().addTrackingChange( TrackingField.WEBSOCKET_INCOMING, converted.length );
websocket.environment().observe( Metrics.WEBSOCKET_INCOMING, converted.length );
websocket.environment().queueEvent( MESSAGE_EVENT, websocket.address(), converted, true );
}
else if( frame instanceof CloseWebSocketFrame )

View File

@ -36,7 +36,6 @@ public class Computer
private String label = null;
// Read-only fields about the computer
private final ComputerEnvironment computerEnvironment;
private final GlobalEnvironment globalEnvironment;
private final Terminal terminal;
private final ComputerExecutor executor;
@ -44,7 +43,7 @@ public class Computer
// Additional state about the computer and its environment.
private boolean blinking = false;
private final Environment internalEnvironment = new Environment( this );
private final Environment internalEnvironment;
private final AtomicBoolean externalOutputChanged = new AtomicBoolean();
private boolean startRequested;
@ -55,16 +54,11 @@ public Computer( GlobalEnvironment globalEnvironment, ComputerEnvironment enviro
if( id < 0 ) throw new IllegalStateException( "Id has not been assigned" );
this.id = id;
this.globalEnvironment = globalEnvironment;
this.computerEnvironment = environment;
this.terminal = terminal;
executor = new ComputerExecutor( this );
serverExecutor = new MainThreadExecutor( this );
}
ComputerEnvironment getComputerEnvironment()
{
return computerEnvironment;
internalEnvironment = new Environment( this, environment );
executor = new ComputerExecutor( this, environment );
serverExecutor = new MainThreadExecutor( environment.getMetrics() );
}
GlobalEnvironment getGlobalEnvironment()

View File

@ -7,6 +7,7 @@
import dan200.computercraft.api.filesystem.IWritableMount;
import dan200.computercraft.core.filesystem.FileMount;
import dan200.computercraft.core.metrics.MetricsObserver;
import javax.annotation.Nullable;
@ -26,6 +27,14 @@ public interface ComputerEnvironment
*/
double getTimeOfDay();
/**
* Get the {@link MetricsObserver} for this computer. This should be constant for the duration of this
* {@link ComputerEnvironment}.
*
* @return This computer's {@link MetricsObserver}.
*/
MetricsObserver getMetrics();
/**
* Construct the mount for this computer's user-writable data.
*

View File

@ -15,9 +15,11 @@
import dan200.computercraft.core.filesystem.FileSystemException;
import dan200.computercraft.core.lua.CobaltLuaMachine;
import dan200.computercraft.core.lua.ILuaMachine;
import dan200.computercraft.core.lua.MachineEnvironment;
import dan200.computercraft.core.lua.MachineResult;
import dan200.computercraft.core.metrics.Metrics;
import dan200.computercraft.core.metrics.MetricsObserver;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.tracking.Tracking;
import dan200.computercraft.shared.util.Colour;
import dan200.computercraft.shared.util.IoUtil;
@ -57,6 +59,8 @@ final class ComputerExecutor
private static final int QUEUE_LIMIT = 256;
private final Computer computer;
private final ComputerEnvironment computerEnvironment;
private final MetricsObserver metrics;
private final List<ILuaAPI> apis = new ArrayList<>();
final TimeoutState timeout = new TimeoutState();
@ -157,12 +161,14 @@ final class ComputerExecutor
*/
final AtomicReference<Thread> executingThread = new AtomicReference<>();
ComputerExecutor( Computer computer )
ComputerExecutor( Computer computer, ComputerEnvironment computerEnvironment )
{
// Ensure the computer thread is running as required.
ComputerThread.start();
this.computer = computer;
this.computerEnvironment = computerEnvironment;
metrics = computerEnvironment.getMetrics();
Environment environment = computer.getEnvironment();
@ -345,7 +351,7 @@ private IMount getRomMount()
private IWritableMount getRootMount()
{
if( rootMount == null ) rootMount = computer.getComputerEnvironment().createRootMount();
if( rootMount == null ) rootMount = computerEnvironment.createRootMount();
return rootMount;
}
@ -395,7 +401,9 @@ private ILuaMachine createLuaMachine()
}
// Create the lua machine
ILuaMachine machine = luaFactory.create( computer, timeout );
ILuaMachine machine = luaFactory.create( new MachineEnvironment(
new LuaContext( computer ), metrics, timeout, computer.getGlobalEnvironment().getHostString()
) );
// 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 );
@ -522,7 +530,7 @@ boolean afterWork()
timeout.stopTimer();
}
Tracking.addTaskTiming( getComputer(), timeout.nanoCurrent() );
metrics.observe( Metrics.COMPUTER_TASKS, timeout.nanoCurrent() );
if( interruptedEvent ) return true;

View File

@ -10,9 +10,9 @@
import dan200.computercraft.api.peripheral.IWorkMonitor;
import dan200.computercraft.core.apis.IAPIEnvironment;
import dan200.computercraft.core.filesystem.FileSystem;
import dan200.computercraft.core.metrics.Metric;
import dan200.computercraft.core.metrics.MetricsObserver;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.tracking.Tracking;
import dan200.computercraft.core.tracking.TrackingField;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
@ -42,6 +42,8 @@
public final class Environment implements IAPIEnvironment
{
private final Computer computer;
private final ComputerEnvironment environment;
private final MetricsObserver metrics;
private boolean internalOutputChanged = false;
private final int[] internalOutput = new int[ComputerSide.COUNT];
@ -60,9 +62,11 @@ public final class Environment implements IAPIEnvironment
private final Int2ObjectMap<Timer> timers = new Int2ObjectOpenHashMap<>();
private int nextTimerToken = 0;
Environment( Computer computer )
Environment( Computer computer, ComputerEnvironment environment )
{
this.computer = computer;
this.environment = environment;
metrics = environment.getMetrics();
}
@Override
@ -75,7 +79,7 @@ public int getComputerID()
@Override
public ComputerEnvironment getComputerEnvironment()
{
return computer.getComputerEnvironment();
return environment;
}
@Nonnull
@ -367,9 +371,15 @@ public void cancelTimer( int id )
}
@Override
public void addTrackingChange( @Nonnull TrackingField field, long change )
public void observe( @Nonnull Metric.Event event, long change )
{
Tracking.addValue( computer, field, change );
metrics.observe( event, change );
}
@Override
public void observe( @Nonnull Metric.Counter counter )
{
metrics.observe( counter );
}
private static class Timer

View File

@ -3,14 +3,12 @@
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.lua;
package dan200.computercraft.core.computer;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.api.lua.ILuaContext;
import dan200.computercraft.api.lua.ILuaTask;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.core.computer.Computer;
import dan200.computercraft.core.computer.MainThread;
import javax.annotation.Nonnull;

View File

@ -7,7 +7,8 @@
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.api.peripheral.IWorkMonitor;
import dan200.computercraft.core.tracking.Tracking;
import dan200.computercraft.core.metrics.Metrics;
import dan200.computercraft.core.metrics.MetricsObserver;
import dan200.computercraft.shared.turtle.core.TurtleBrain;
import net.minecraft.tileentity.TileEntity;
@ -56,7 +57,7 @@ final class MainThreadExecutor implements IWorkMonitor
*/
private static final int MAX_TASKS = 5000;
private final Computer computer;
private final MetricsObserver metrics;
/**
* A lock used for any changes to {@link #tasks}, or {@link #onQueue}. This will be
@ -109,9 +110,9 @@ final class MainThreadExecutor implements IWorkMonitor
long virtualTime;
MainThreadExecutor( Computer computer )
MainThreadExecutor( MetricsObserver metrics )
{
this.computer = computer;
this.metrics = metrics;
}
/**
@ -194,7 +195,7 @@ public void trackWork( long time, @Nonnull TimeUnit unit )
private void consumeTime( long time )
{
Tracking.addServerTiming( computer, time );
metrics.observe( Metrics.SERVER_TASKS, time );
// 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.

View File

@ -12,10 +12,9 @@
import dan200.computercraft.api.lua.ILuaFunction;
import dan200.computercraft.core.asm.LuaMethod;
import dan200.computercraft.core.asm.ObjectSource;
import dan200.computercraft.core.computer.Computer;
import dan200.computercraft.core.computer.TimeoutState;
import dan200.computercraft.core.tracking.Tracking;
import dan200.computercraft.core.tracking.TrackingField;
import dan200.computercraft.core.metrics.Metrics;
import dan200.computercraft.core.metrics.MetricsObserver;
import dan200.computercraft.shared.util.ThreadUtils;
import org.squiddev.cobalt.*;
import org.squiddev.cobalt.compiler.CompileException;
@ -52,7 +51,6 @@ public class CobaltLuaMachine implements ILuaMachine
private static final LuaMethod FUNCTION_METHOD = ( target, context, args ) -> ((ILuaFunction) target).call( args );
private final Computer computer;
private final TimeoutState timeout;
private final TimeoutDebugHandler debug;
private final ILuaContext context;
@ -63,19 +61,19 @@ public class CobaltLuaMachine implements ILuaMachine
private LuaThread mainRoutine = null;
private String eventFilter = null;
public CobaltLuaMachine( Computer computer, TimeoutState timeout )
public CobaltLuaMachine( MachineEnvironment environment )
{
this.computer = computer;
this.timeout = timeout;
context = new LuaContext( computer );
timeout = environment.timeout;
context = environment.context;
debug = new TimeoutDebugHandler();
// Create an environment to run in
MetricsObserver metrics = environment.metrics;
LuaState state = this.state = LuaState.builder()
.resourceManipulator( new VoidResourceManipulator() )
.debug( debug )
.coroutineExecutor( command -> {
Tracking.addValue( this.computer, TrackingField.COROUTINES_CREATED, 1 );
metrics.observe( Metrics.COROUTINES_CREATED );
COROUTINES.execute( () -> {
try
{
@ -83,7 +81,7 @@ public CobaltLuaMachine( Computer computer, TimeoutState timeout )
}
finally
{
Tracking.addValue( this.computer, TrackingField.COROUTINES_DISPOSED, 1 );
metrics.observe( Metrics.COROUTINES_DISPOSED );
}
} );
} )
@ -110,7 +108,7 @@ public CobaltLuaMachine( Computer computer, TimeoutState timeout )
// Add version globals
globals.rawset( "_VERSION", valueOf( "Lua 5.1" ) );
globals.rawset( "_HOST", valueOf( computer.getAPIEnvironment().getGlobalEnvironment().getHostString() ) );
globals.rawset( "_HOST", valueOf( environment.hostString ) );
globals.rawset( "_CC_DEFAULT_SETTINGS", valueOf( ComputerCraft.defaultComputerSettings ) );
if( ComputerCraft.disableLua51Features )
{

View File

@ -7,8 +7,6 @@
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;
@ -77,6 +75,6 @@ public interface ILuaMachine
interface Factory
{
ILuaMachine create( Computer computer, TimeoutState timeout );
ILuaMachine create( MachineEnvironment environment );
}
}

View File

@ -0,0 +1,49 @@
/*
* 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.lua;
import dan200.computercraft.api.lua.ILuaContext;
import dan200.computercraft.core.computer.GlobalEnvironment;
import dan200.computercraft.core.computer.TimeoutState;
import dan200.computercraft.core.metrics.Metrics;
import dan200.computercraft.core.metrics.MetricsObserver;
/**
* Arguments used to construct a {@link ILuaMachine}.
*
* @see ILuaMachine.Factory
*/
public class MachineEnvironment
{
/**
* The Lua context to execute main-thread tasks with.
*/
public final ILuaContext context;
/**
* A sink to submit metrics to. You do not need to submit task timings here, it should only be for additional
* metrics such as {@link Metrics#COROUTINES_CREATED}
*/
public final MetricsObserver metrics;
/**
* The current timeout state. This should be used by the machine to interrupt its execution.
*/
public final TimeoutState timeout;
/**
* A {@linkplain GlobalEnvironment#getHostString() host string} to identify the current environment.
*/
public final String hostString;
public MachineEnvironment( ILuaContext context, MetricsObserver metrics, TimeoutState timeout, String hostString )
{
this.context = context;
this.metrics = metrics;
this.timeout = timeout;
this.hostString = hostString;
}
}

View File

@ -0,0 +1,141 @@
/*
* 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.metrics;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.LongFunction;
/**
* A metric is some event which is emitted by a computer and observed by a {@link MetricsObserver}.
* <p>
* It comes in two forms: a simple {@link Metric.Counter} counts how many times an event has occurred (for instance,
* how many HTTP requests have there been) while a {@link Metric.Event} has a discrete value for each event (for
* instance, the number of bytes downloaded in this HTTP request). The values tied to a {@link Metric.Event}s can be
* accumulated, and thus used to derive averages, rates and be sorted into histogram buckets.
*/
public abstract class Metric
{
private static final Map<String, Metric> allMetrics = new ConcurrentHashMap<>();
private static final AtomicInteger nextId = new AtomicInteger();
private final int id;
private final String name;
private final String unit;
private final LongFunction<String> format;
private Metric( String name, String unit, LongFunction<String> format )
{
if( allMetrics.containsKey( name ) ) throw new IllegalStateException( "Duplicate key " + name );
id = nextId.getAndIncrement();
this.name = name;
this.unit = unit;
this.format = format;
allMetrics.put( name, this );
}
/**
* The unique ID for this metric.
* <p>
* Each metric is assigned a consecutive metric ID starting from 0. This allows you to store metrics in an array,
* rather than a map.
*
* @return The metric's ID.
*/
public int id()
{
return id;
}
/**
* The unique name for this metric.
*
* @return The metric's name.
*/
public String name()
{
return name;
}
/**
* The unit used for this metric. This should be a lowercase "identifier-like" such as {@literal ms}. If no unit is
* relevant, it can be empty.
*
* @return The metric's unit.
*/
public String unit()
{
return unit;
}
/**
* Format a value according to the metric's formatting rules. Implementations may choose to append units to the
* returned value where relevant.
*
* @param value The value to format.
* @return The formatted value.
*/
public String format( long value )
{
return format.apply( value );
}
@Override
public String toString()
{
return getClass().getName() + ":" + name();
}
/**
* Get a map of all metrics.
*
* @return A map of all metrics.
*/
public static Map<String, Metric> metrics()
{
return Collections.unmodifiableMap( allMetrics );
}
public static final class Counter extends Metric
{
public Counter( String id )
{
super( id, "", Metric::formatDefault );
}
}
public static final class Event extends Metric
{
public Event( String id, String unit, LongFunction<String> format )
{
super( id, unit, format );
}
}
public static String formatTime( long value )
{
return String.format( "%.1fms", value * 1e-6 );
}
public static String formatDefault( long value )
{
return String.format( "%d", value );
}
private static final int KILOBYTE_SIZE = 1024;
private static final String SI_PREFIXES = "KMGT";
public static String formatBytes( long bytes )
{
if( bytes < KILOBYTE_SIZE ) return String.format( "%d B", bytes );
int exp = (int) (Math.log( (double) bytes ) / Math.log( KILOBYTE_SIZE ));
if( exp > SI_PREFIXES.length() ) exp = SI_PREFIXES.length();
return String.format( "%.1f %siB", bytes / Math.pow( KILOBYTE_SIZE, exp ), SI_PREFIXES.charAt( exp - 1 ) );
}
}

View File

@ -0,0 +1,41 @@
/*
* 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.metrics;
/**
* Built-in metrics that CC produces.
*/
public final class Metrics
{
private Metrics()
{
}
public static final Metric.Event COMPUTER_TASKS = new Metric.Event( "computer_tasks", "ms", Metric::formatTime );
public static final Metric.Event SERVER_TASKS = new Metric.Event( "server_tasks", "ms", Metric::formatTime );
public static final Metric.Counter PERIPHERAL_OPS = new Metric.Counter( "peripheral" );
public static final Metric.Counter FS_OPS = new Metric.Counter( "fs" );
public static final Metric.Counter HTTP_REQUESTS = new Metric.Counter( "http_requests" );
public static final Metric.Event HTTP_UPLOAD = new Metric.Event( "http_upload", "bytes", Metric::formatBytes );
public static final Metric.Event HTTP_DOWNLOAD = new Metric.Event( "http_download", "bytes", Metric::formatBytes );
public static final Metric.Event WEBSOCKET_INCOMING = new Metric.Event( "websocket_incoming", "bytes", Metric::formatBytes );
public static final Metric.Event WEBSOCKET_OUTGOING = new Metric.Event( "websocket_outgoing", "bytes", Metric::formatBytes );
public static final Metric.Counter COROUTINES_CREATED = new Metric.Counter( "coroutines_created" );
public static final Metric.Counter COROUTINES_DISPOSED = new Metric.Counter( "coroutines_dead" );
public static final Metric.Counter TURTLE_OPS = new Metric.Counter( "turtle_ops" );
/**
* Ensures metrics are registered.
*/
public static void init()
{
}
}

View File

@ -0,0 +1,36 @@
/*
* 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.metrics;
import dan200.computercraft.core.computer.ComputerEnvironment;
/**
* A metrics observer is used to report metrics for a single computer.
* <p>
* Various components (such as the computer scheduler or http API) will report statistics about their behaviour to the
* observer. The observer may choose to consume these metrics, aggregating them and presenting them to the user in some
* manner.
*
* @see ComputerEnvironment#getMetrics()
* @see Metrics Built-in metrics which will be reported.
*/
public interface MetricsObserver
{
/**
* Increment a counter by 1.
*
* @param counter The counter to observe.
*/
void observe( Metric.Counter counter );
/**
* Observe a single instance of an event.
*
* @param event The event to observe.
* @param value The value corresponding to this event.
*/
void observe( Metric.Event event, long value );
}

View File

@ -1,156 +0,0 @@
/*
* 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.tracking;
import com.google.common.base.CaseFormat;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.core.computer.Computer;
import net.minecraft.util.text.LanguageMap;
import javax.annotation.Nonnull;
import javax.management.*;
import java.lang.management.ManagementFactory;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.LongSupplier;
public final class ComputerMBean implements DynamicMBean, Tracker
{
private static final Set<TrackingField> SKIP = new HashSet<>( Arrays.asList(
TrackingField.TASKS, TrackingField.TOTAL_TIME, TrackingField.AVERAGE_TIME, TrackingField.MAX_TIME,
TrackingField.SERVER_COUNT, TrackingField.SERVER_TIME
) );
private static ComputerMBean instance;
private final Map<String, LongSupplier> attributes = new HashMap<>();
private final Map<TrackingField, Counter> values = new HashMap<>();
private final MBeanInfo info;
private ComputerMBean()
{
List<MBeanAttributeInfo> attributes = new ArrayList<>();
for( Map.Entry<String, TrackingField> field : TrackingField.fields().entrySet() )
{
if( SKIP.contains( field.getValue() ) ) continue;
String name = CaseFormat.LOWER_UNDERSCORE.to( CaseFormat.LOWER_CAMEL, field.getKey() );
add( name, field.getValue(), attributes, null );
}
add( "task", TrackingField.TOTAL_TIME, attributes, TrackingField.TASKS );
add( "serverTask", TrackingField.SERVER_TIME, attributes, TrackingField.SERVER_COUNT );
this.info = new MBeanInfo(
ComputerMBean.class.getSimpleName(),
"metrics about all computers on the server",
attributes.toArray( new MBeanAttributeInfo[0] ), null, null, null
);
}
public static void register()
{
try
{
ManagementFactory.getPlatformMBeanServer().registerMBean( instance = new ComputerMBean(), new ObjectName( "dan200.computercraft:type=Computers" ) );
}
catch( InstanceAlreadyExistsException | MBeanRegistrationException | NotCompliantMBeanException |
MalformedObjectNameException e )
{
ComputerCraft.log.warn( "Failed to register JMX bean", e );
}
}
public static void registerTracker()
{
if( instance != null ) Tracking.add( instance );
}
@Override
public Object getAttribute( String attribute ) throws AttributeNotFoundException, MBeanException, ReflectionException
{
LongSupplier value = attributes.get( attribute );
if( value == null ) throw new AttributeNotFoundException();
return value.getAsLong();
}
@Override
public void setAttribute( Attribute attribute ) throws InvalidAttributeValueException
{
throw new InvalidAttributeValueException( "Cannot set attribute" );
}
@Override
public AttributeList getAttributes( String[] attributes )
{
return null;
}
@Override
public AttributeList setAttributes( AttributeList attributes )
{
return new AttributeList();
}
@Override
public Object invoke( String actionName, Object[] params, String[] signature ) throws MBeanException, ReflectionException
{
return null;
}
@Override
@Nonnull
public MBeanInfo getMBeanInfo()
{
return info;
}
@Override
public void addTaskTiming( Computer computer, long time )
{
addValue( computer, TrackingField.TOTAL_TIME, time );
}
@Override
public void addServerTiming( Computer computer, long time )
{
addValue( computer, TrackingField.SERVER_TIME, time );
}
@Override
public void addValue( Computer computer, TrackingField field, long change )
{
Counter counter = values.get( field );
counter.value.addAndGet( change );
counter.count.incrementAndGet();
}
private MBeanAttributeInfo addAttribute( String name, String description, LongSupplier value )
{
attributes.put( name, value );
return new MBeanAttributeInfo( name, "long", description, true, false, false );
}
private void add( String name, TrackingField field, List<MBeanAttributeInfo> attributes, TrackingField count )
{
Counter counter = new Counter();
values.put( field, counter );
String prettyName = LanguageMap.getInstance().getOrDefault( field.translationKey() );
attributes.add( addAttribute( name, prettyName, counter.value::longValue ) );
if( count != null )
{
String countName = LanguageMap.getInstance().getOrDefault( count.translationKey() );
attributes.add( addAttribute( name + "Count", countName, counter.count::longValue ) );
}
}
private static class Counter
{
AtomicLong value = new AtomicLong();
AtomicLong count = new AtomicLong();
}
}

View File

@ -1,122 +0,0 @@
/*
* 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.tracking;
import dan200.computercraft.core.computer.Computer;
import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap;
import javax.annotation.Nullable;
import java.lang.ref.WeakReference;
public class ComputerTracker
{
private final WeakReference<Computer> computer;
private final int computerId;
private long tasks;
private long totalTime;
private long maxTime;
private long serverCount;
private long serverTime;
private final Object2LongOpenHashMap<TrackingField> fields;
public ComputerTracker( Computer computer )
{
this.computer = new WeakReference<>( computer );
computerId = computer.getID();
fields = new Object2LongOpenHashMap<>();
}
ComputerTracker( ComputerTracker timings )
{
computer = timings.computer;
computerId = timings.computerId;
tasks = timings.tasks;
totalTime = timings.totalTime;
maxTime = timings.maxTime;
serverCount = timings.serverCount;
serverTime = timings.serverTime;
fields = new Object2LongOpenHashMap<>( timings.fields );
}
@Nullable
public Computer getComputer()
{
return computer.get();
}
public int getComputerId()
{
return computerId;
}
public long getTasks()
{
return tasks;
}
public long getTotalTime()
{
return totalTime;
}
public long getMaxTime()
{
return maxTime;
}
public long getAverage()
{
return totalTime / tasks;
}
void addTaskTiming( long time )
{
tasks++;
totalTime += time;
if( time > maxTime ) maxTime = time;
}
void addMainTiming( long time )
{
serverCount++;
serverTime += time;
}
void addValue( TrackingField field, long change )
{
synchronized( fields )
{
fields.addTo( field, change );
}
}
public long get( TrackingField field )
{
if( field == TrackingField.TASKS ) return tasks;
if( field == TrackingField.MAX_TIME ) return maxTime;
if( field == TrackingField.TOTAL_TIME ) return totalTime;
if( field == TrackingField.AVERAGE_TIME ) return tasks == 0 ? 0 : totalTime / tasks;
if( field == TrackingField.SERVER_COUNT ) return serverCount;
if( field == TrackingField.SERVER_TIME ) return serverTime;
synchronized( fields )
{
return fields.getLong( field );
}
}
public String getFormatted( TrackingField field )
{
return field.format( get( field ) );
}
}

View File

@ -1,47 +0,0 @@
/*
* 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.tracking;
import dan200.computercraft.core.computer.Computer;
public interface Tracker
{
/**
* Report how long a task executed on the computer thread took.
*
* Computer thread tasks include events or a computer being turned on/off.
*
* @param computer The computer processing this task
* @param time The time taken for this task.
*/
default void addTaskTiming( Computer computer, long time )
{
}
/**
* Report how long a task executed on the server thread took.
*
* Server tasks include actions performed by peripherals.
*
* @param computer The computer processing this task
* @param time The time taken for this task.
*/
default void addServerTiming( Computer computer, long time )
{
}
/**
* Increment an arbitrary field by some value. Implementations may track how often this is called
* as well as the change, to compute some level of "average".
*
* @param computer The computer to increment
* @param field The field to increment.
* @param change The amount to increment said field by.
*/
default void addValue( Computer computer, TrackingField field, long change )
{
}
}

View File

@ -1,87 +0,0 @@
/*
* 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.tracking;
import dan200.computercraft.core.computer.Computer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
public final class Tracking
{
static final AtomicInteger tracking = new AtomicInteger( 0 );
private static final Object lock = new Object();
private static final HashMap<UUID, TrackingContext> contexts = new HashMap<>();
private static final List<Tracker> trackers = new ArrayList<>();
private Tracking() {}
public static TrackingContext getContext( UUID uuid )
{
synchronized( lock )
{
TrackingContext context = contexts.get( uuid );
if( context == null ) contexts.put( uuid, context = new TrackingContext() );
return context;
}
}
public static void add( Tracker tracker )
{
synchronized( lock )
{
trackers.add( tracker );
tracking.incrementAndGet();
}
}
public static void addTaskTiming( Computer computer, long time )
{
if( tracking.get() == 0 ) return;
synchronized( contexts )
{
for( TrackingContext context : contexts.values() ) context.addTaskTiming( computer, time );
for( Tracker tracker : trackers ) tracker.addTaskTiming( computer, time );
}
}
public static void addServerTiming( Computer computer, long time )
{
if( tracking.get() == 0 ) return;
synchronized( contexts )
{
for( TrackingContext context : contexts.values() ) context.addServerTiming( computer, time );
for( Tracker tracker : trackers ) tracker.addServerTiming( computer, time );
}
}
public static void addValue( Computer computer, TrackingField field, long change )
{
if( tracking.get() == 0 ) return;
synchronized( lock )
{
for( TrackingContext context : contexts.values() ) context.addValue( computer, field, change );
for( Tracker tracker : trackers ) tracker.addValue( computer, field, change );
}
}
public static void reset()
{
synchronized( lock )
{
contexts.clear();
trackers.clear();
tracking.set( 0 );
}
}
}

View File

@ -1,116 +0,0 @@
/*
* 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.tracking;
import com.google.common.collect.MapMaker;
import dan200.computercraft.core.computer.Computer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Tracks timing information about computers, including how long they ran for
* and the number of events they handled.
*
* Note that this <em>will</em> track computers which have been deleted (hence
* the presence of {@link #timingLookup} and {@link #timings}
*/
public class TrackingContext implements Tracker
{
private boolean tracking = false;
private final List<ComputerTracker> timings = new ArrayList<>();
private final Map<Computer, ComputerTracker> timingLookup = new MapMaker().weakKeys().makeMap();
public synchronized void start()
{
if( !tracking ) Tracking.tracking.incrementAndGet();
tracking = true;
timings.clear();
timingLookup.clear();
}
public synchronized boolean stop()
{
if( !tracking ) return false;
Tracking.tracking.decrementAndGet();
tracking = false;
timingLookup.clear();
return true;
}
public synchronized List<ComputerTracker> getImmutableTimings()
{
ArrayList<ComputerTracker> timings = new ArrayList<>( this.timings.size() );
for( ComputerTracker timing : this.timings ) timings.add( new ComputerTracker( timing ) );
return timings;
}
public synchronized List<ComputerTracker> getTimings()
{
return new ArrayList<>( timings );
}
@Override
public void addTaskTiming( Computer computer, long time )
{
if( !tracking ) return;
synchronized( this )
{
ComputerTracker computerTimings = timingLookup.get( computer );
if( computerTimings == null )
{
computerTimings = new ComputerTracker( computer );
timingLookup.put( computer, computerTimings );
timings.add( computerTimings );
}
computerTimings.addTaskTiming( time );
}
}
@Override
public void addServerTiming( Computer computer, long time )
{
if( !tracking ) return;
synchronized( this )
{
ComputerTracker computerTimings = timingLookup.get( computer );
if( computerTimings == null )
{
computerTimings = new ComputerTracker( computer );
timingLookup.put( computer, computerTimings );
timings.add( computerTimings );
}
computerTimings.addMainTiming( time );
}
}
@Override
public void addValue( Computer computer, TrackingField field, long change )
{
if( !tracking ) return;
synchronized( this )
{
ComputerTracker computerTimings = timingLookup.get( computer );
if( computerTimings == null )
{
computerTimings = new ComputerTracker( computer );
timingLookup.put( computer, computerTimings );
timings.add( computerTimings );
}
computerTimings.addValue( field, change );
}
}
}

View File

@ -1,96 +0,0 @@
/*
* 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.tracking;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.LongFunction;
public final class TrackingField
{
private static final Map<String, TrackingField> fields = new HashMap<>();
public static final TrackingField TASKS = TrackingField.of( "tasks", x -> String.format( "%4d", x ) );
public static final TrackingField TOTAL_TIME = TrackingField.of( "total", x -> String.format( "%7.1fms", x / 1e6 ) );
public static final TrackingField AVERAGE_TIME = TrackingField.of( "average", x -> String.format( "%4.1fms", x / 1e6 ) );
public static final TrackingField MAX_TIME = TrackingField.of( "max", x -> String.format( "%5.1fms", x / 1e6 ) );
public static final TrackingField SERVER_COUNT = TrackingField.of( "server_count", x -> String.format( "%4d", x ) );
public static final TrackingField SERVER_TIME = TrackingField.of( "server_time", x -> String.format( "%7.1fms", x / 1e6 ) );
public static final TrackingField PERIPHERAL_OPS = TrackingField.of( "peripheral", TrackingField::formatDefault );
public static final TrackingField FS_OPS = TrackingField.of( "fs", TrackingField::formatDefault );
public static final TrackingField TURTLE_OPS = TrackingField.of( "turtle", TrackingField::formatDefault );
public static final TrackingField HTTP_REQUESTS = TrackingField.of( "http", TrackingField::formatDefault );
public static final TrackingField HTTP_UPLOAD = TrackingField.of( "http_upload", TrackingField::formatBytes );
public static final TrackingField HTTP_DOWNLOAD = TrackingField.of( "http_download", TrackingField::formatBytes );
public static final TrackingField WEBSOCKET_INCOMING = TrackingField.of( "websocket_incoming", TrackingField::formatBytes );
public static final TrackingField WEBSOCKET_OUTGOING = TrackingField.of( "websocket_outgoing", TrackingField::formatBytes );
public static final TrackingField COROUTINES_CREATED = TrackingField.of( "coroutines_created", x -> String.format( "%4d", x ) );
public static final TrackingField COROUTINES_DISPOSED = TrackingField.of( "coroutines_dead", x -> String.format( "%4d", x ) );
private final String id;
private final String translationKey;
private final LongFunction<String> format;
public String id()
{
return id;
}
public String translationKey()
{
return translationKey;
}
private TrackingField( String id, LongFunction<String> format )
{
this.id = id;
translationKey = "tracking_field.computercraft." + id + ".name";
this.format = format;
}
public String format( long value )
{
return format.apply( value );
}
public static TrackingField of( String id, LongFunction<String> format )
{
TrackingField field = new TrackingField( id, format );
fields.put( id, field );
return field;
}
public static Map<String, TrackingField> fields()
{
return Collections.unmodifiableMap( fields );
}
private static String formatDefault( long value )
{
return String.format( "%6d", value );
}
/**
* So technically a kibibyte, but let's not argue here.
*/
private static final int KILOBYTE_SIZE = 1024;
private static final String SI_PREFIXES = "KMGT";
private static String formatBytes( long bytes )
{
if( bytes < 1024 ) return String.format( "%10d B", bytes );
int exp = (int) (Math.log( bytes ) / Math.log( KILOBYTE_SIZE ));
if( exp > SI_PREFIXES.length() ) exp = SI_PREFIXES.length();
return String.format( "%10.1f %siB", bytes / Math.pow( KILOBYTE_SIZE, exp ), SI_PREFIXES.charAt( exp - 1 ) );
}
}

View File

@ -9,10 +9,9 @@
import dan200.computercraft.core.apis.http.NetworkUtils;
import dan200.computercraft.core.computer.MainThread;
import dan200.computercraft.core.filesystem.ResourceMount;
import dan200.computercraft.core.tracking.ComputerMBean;
import dan200.computercraft.core.tracking.Tracking;
import dan200.computercraft.shared.command.CommandComputerCraft;
import dan200.computercraft.shared.computer.core.ServerContext;
import dan200.computercraft.shared.computer.metrics.ComputerMBean;
import dan200.computercraft.shared.peripheral.modem.wireless.WirelessNetwork;
import net.minecraft.entity.EntityType;
import net.minecraft.loot.ConstantRange;
@ -74,7 +73,7 @@ public static void onServerStarting( FMLServerStartingEvent event )
resetState();
ServerContext.create( server );
ComputerMBean.registerTracker();
ComputerMBean.start( server );
}
@SubscribeEvent
@ -88,7 +87,6 @@ private static void resetState()
ServerContext.close();
MainThread.reset();
WirelessNetwork.resetNetworks();
Tracking.reset();
NetworkUtils.reset();
}

View File

@ -9,17 +9,17 @@
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.core.computer.Computer;
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.core.tracking.ComputerTracker;
import dan200.computercraft.core.tracking.Tracking;
import dan200.computercraft.core.tracking.TrackingContext;
import dan200.computercraft.core.tracking.TrackingField;
import dan200.computercraft.core.metrics.Metrics;
import dan200.computercraft.shared.command.text.TableBuilder;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.core.ServerContext;
import dan200.computercraft.shared.computer.inventory.ContainerViewComputer;
import dan200.computercraft.shared.computer.metrics.basic.Aggregate;
import dan200.computercraft.shared.computer.metrics.basic.AggregatedMetric;
import dan200.computercraft.shared.computer.metrics.basic.BasicComputerMetricsObserver;
import dan200.computercraft.shared.computer.metrics.basic.ComputerMetrics;
import dan200.computercraft.shared.network.container.ComputerContainerData;
import net.minecraft.command.CommandSource;
import net.minecraft.entity.Entity;
@ -46,7 +46,7 @@
import static dan200.computercraft.shared.command.arguments.ComputerArgumentType.getComputerArgument;
import static dan200.computercraft.shared.command.arguments.ComputerArgumentType.oneComputer;
import static dan200.computercraft.shared.command.arguments.ComputersArgumentType.*;
import static dan200.computercraft.shared.command.arguments.TrackingFieldArgumentType.trackingField;
import static dan200.computercraft.shared.command.arguments.TrackingFieldArgumentType.metric;
import static dan200.computercraft.shared.command.builder.CommandBuilder.args;
import static dan200.computercraft.shared.command.builder.CommandBuilder.command;
import static dan200.computercraft.shared.command.builder.HelpingArgumentBuilder.choice;
@ -248,7 +248,7 @@ public Container createMenu( int id, @Nonnull PlayerInventory player, @Nonnull P
.then( command( "start" )
.requires( UserLevel.OWNER_OP )
.executes( context -> {
getTimingContext( context.getSource() ).start();
getMetricsInstance( context.getSource() ).start();
String stopCommand = "/computercraft track stop";
context.getSource().sendSuccess( translate( "commands.computercraft.track.start.stop",
@ -259,17 +259,17 @@ public Container createMenu( int id, @Nonnull PlayerInventory player, @Nonnull P
.then( command( "stop" )
.requires( UserLevel.OWNER_OP )
.executes( context -> {
TrackingContext timings = getTimingContext( context.getSource() );
BasicComputerMetricsObserver timings = getMetricsInstance( context.getSource() );
if( !timings.stop() ) throw NOT_TRACKING_EXCEPTION.create();
displayTimings( context.getSource(), timings.getImmutableTimings(), TrackingField.AVERAGE_TIME, DEFAULT_FIELDS );
displayTimings( context.getSource(), timings.getSnapshot(), new AggregatedMetric( Metrics.COMPUTER_TASKS, Aggregate.AVG ), DEFAULT_FIELDS );
return 1;
} ) )
.then( command( "dump" )
.requires( UserLevel.OWNER_OP )
.argManyValue( "fields", trackingField(), DEFAULT_FIELDS )
.argManyValue( "fields", metric(), DEFAULT_FIELDS )
.executes( ( context, fields ) -> {
TrackingField sort;
AggregatedMetric sort;
if( fields.size() == 1 && DEFAULT_FIELDS.contains( fields.get( 0 ) ) )
{
sort = fields.get( 0 );
@ -362,50 +362,48 @@ private static ITextComponent linkStorage( CommandSource source, int id )
}
@Nonnull
private static TrackingContext getTimingContext( CommandSource source )
private static BasicComputerMetricsObserver getMetricsInstance( CommandSource source )
{
Entity entity = source.getEntity();
return entity instanceof PlayerEntity ? Tracking.getContext( entity.getUUID() ) : Tracking.getContext( SYSTEM_UUID );
return ServerContext.get( source.getServer() ).metrics().getMetricsInstance( entity instanceof PlayerEntity ? entity.getUUID() : SYSTEM_UUID );
}
private static final List<TrackingField> DEFAULT_FIELDS = Arrays.asList( TrackingField.TASKS, TrackingField.TOTAL_TIME, TrackingField.AVERAGE_TIME, TrackingField.MAX_TIME );
private static final List<AggregatedMetric> DEFAULT_FIELDS = Arrays.asList(
new AggregatedMetric( Metrics.COMPUTER_TASKS, Aggregate.COUNT ),
new AggregatedMetric( Metrics.COMPUTER_TASKS, Aggregate.NONE ),
new AggregatedMetric( Metrics.COMPUTER_TASKS, Aggregate.AVG ),
new AggregatedMetric( Metrics.COMPUTER_TASKS, Aggregate.MAX )
);
private static int displayTimings( CommandSource source, TrackingField sortField, List<TrackingField> fields ) throws CommandSyntaxException
private static int displayTimings( CommandSource source, AggregatedMetric sortField, List<AggregatedMetric> fields ) throws CommandSyntaxException
{
return displayTimings( source, getTimingContext( source ).getTimings(), sortField, fields );
return displayTimings( source, getMetricsInstance( source ).getTimings(), sortField, fields );
}
private static int displayTimings( CommandSource source, @Nonnull List<ComputerTracker> timings, @Nonnull TrackingField sortField, @Nonnull List<TrackingField> fields ) throws CommandSyntaxException
private static int displayTimings( CommandSource source, List<ComputerMetrics> timings, AggregatedMetric sortField, List<AggregatedMetric> fields ) throws CommandSyntaxException
{
if( timings.isEmpty() ) throw NO_TIMINGS_EXCEPTION.create();
Map<Computer, ServerComputer> lookup = new HashMap<>();
int maxId = 0, maxInstance = 0;
for( ServerComputer server : ServerContext.get( source.getServer() ).registry().getComputers() )
{
lookup.put( server.getComputer(), server );
if( server.getInstanceID() > maxInstance ) maxInstance = server.getInstanceID();
if( server.getID() > maxId ) maxId = server.getID();
}
timings.sort( Comparator.<ComputerTracker, Long>comparing( x -> x.get( sortField ) ).reversed() );
timings.sort( Comparator.<ComputerMetrics, Long>comparing( x -> x.get( sortField.metric(), sortField.aggregate() ) ).reversed() );
ITextComponent[] headers = new ITextComponent[1 + fields.size()];
headers[0] = translate( "commands.computercraft.track.dump.computer" );
for( int i = 0; i < fields.size(); i++ ) headers[i + 1] = translate( fields.get( i ).translationKey() );
for( int i = 0; i < fields.size(); i++ ) headers[i + 1] = fields.get( i ).displayName();
TableBuilder table = new TableBuilder( TRACK_ID, headers );
for( ComputerTracker entry : timings )
for( ComputerMetrics entry : timings )
{
Computer computer = entry.getComputer();
ServerComputer serverComputer = computer == null ? null : lookup.get( computer );
ServerComputer serverComputer = entry.computer();
ITextComponent computerComponent = linkComputer( source, serverComputer, entry.getComputerId() );
ITextComponent computerComponent = linkComputer( source, serverComputer, entry.computerId() );
ITextComponent[] row = new ITextComponent[1 + fields.size()];
row[0] = computerComponent;
for( int i = 0; i < fields.size(); i++ ) row[i + 1] = text( entry.getFormatted( fields.get( i ) ) );
for( int i = 0; i < fields.size(); i++ )
{
AggregatedMetric metric = fields.get( i );
row[i + 1] = text( entry.getFormatted( metric.metric(), metric.aggregate() ) );
}
table.row( row );
}

View File

@ -32,7 +32,7 @@ private static <T extends ArgumentType<?>> void register( ResourceLocation id, T
public static void register()
{
register( new ResourceLocation( ComputerCraft.MOD_ID, "tracking_field" ), TrackingFieldArgumentType.trackingField() );
register( new ResourceLocation( ComputerCraft.MOD_ID, "tracking_field" ), TrackingFieldArgumentType.metric() );
register( new ResourceLocation( ComputerCraft.MOD_ID, "computer" ), ComputerArgumentType.oneComputer() );
register( new ResourceLocation( ComputerCraft.MOD_ID, "computers" ), ComputersArgumentType.class, new ComputersArgumentType.Serializer() );
registerUnsafe( new ResourceLocation( ComputerCraft.MOD_ID, "repeat" ), RepeatArgumentType.class, new RepeatArgumentType.Serializer() );

View File

@ -5,21 +5,24 @@
*/
package dan200.computercraft.shared.command.arguments;
import dan200.computercraft.core.tracking.TrackingField;
import dan200.computercraft.shared.command.Exceptions;
import dan200.computercraft.shared.computer.metrics.basic.AggregatedMetric;
import static dan200.computercraft.shared.command.text.ChatHelpers.translate;
import java.util.stream.Collectors;
public final class TrackingFieldArgumentType extends ChoiceArgumentType<TrackingField>
public final class TrackingFieldArgumentType extends ChoiceArgumentType<AggregatedMetric>
{
private static final TrackingFieldArgumentType INSTANCE = new TrackingFieldArgumentType();
private TrackingFieldArgumentType()
{
super( TrackingField.fields().values(), TrackingField::id, x -> translate( x.translationKey() ), Exceptions.TRACKING_FIELD_ARG_NONE );
super(
AggregatedMetric.aggregatedMetrics().collect( Collectors.toList() ),
AggregatedMetric::name, AggregatedMetric::displayName, Exceptions.TRACKING_FIELD_ARG_NONE
);
}
public static TrackingFieldArgumentType trackingField()
public static TrackingFieldArgumentType metric()
{
return INSTANCE;
}

View File

@ -14,6 +14,7 @@
import dan200.computercraft.core.computer.Computer;
import dan200.computercraft.core.computer.ComputerEnvironment;
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.core.metrics.MetricsObserver;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.shared.computer.menu.ComputerMenu;
import dan200.computercraft.shared.network.NetworkHandler;
@ -37,6 +38,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment
private BlockPos position;
private final ComputerFamily family;
private final MetricsObserver metrics;
private final Computer computer;
private final Terminal terminal;
@ -53,6 +55,7 @@ public ServerComputer( ServerWorld world, int computerID, String label, Computer
ServerContext context = ServerContext.get( world.getServer() );
instanceID = context.registry().getUnusedInstanceID();
terminal = new Terminal( terminalWidth, terminalHeight, family != ComputerFamily.NORMAL, this::markTerminalChanged );
metrics = context.metrics().createMetricObserver( this );
computer = new Computer( context.environment(), this, terminal, computerID );
computer.setLabel( label );
@ -262,8 +265,6 @@ public void setLabel( String label )
computer.setLabel( label );
}
// IComputerEnvironment implementation
@Override
public double getTimeOfDay()
{
@ -276,6 +277,12 @@ public int getDay()
return (int) ((world.getDayTime() + 6000) / 24000) + 1;
}
@Override
public MetricsObserver getMetrics()
{
return metrics;
}
@Override
public IWritableMount createRootMount()
{

View File

@ -11,6 +11,7 @@
import dan200.computercraft.api.filesystem.IMount;
import dan200.computercraft.core.computer.GlobalEnvironment;
import dan200.computercraft.shared.CommonHooks;
import dan200.computercraft.shared.computer.metrics.GlobalMetrics;
import dan200.computercraft.shared.util.IDAssigner;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.World;
@ -42,6 +43,7 @@ public final class ServerContext
private final MinecraftServer server;
private final ServerComputerRegistry registry = new ServerComputerRegistry();
private final GlobalMetrics metrics = new GlobalMetrics();
private final GlobalEnvironment environment;
private final IDAssigner idAssigner;
private final Path storageDir;
@ -145,6 +147,16 @@ public Path storageDir()
return storageDir;
}
/**
* Get the current global metrics store.
*
* @return The current metrics store.
*/
public GlobalMetrics metrics()
{
return metrics;
}
private static final class Environment implements GlobalEnvironment
{
private final MinecraftServer server;

View File

@ -0,0 +1,167 @@
/*
* 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.shared.computer.metrics;
import com.google.common.base.CaseFormat;
import dan200.computercraft.core.metrics.Metric;
import dan200.computercraft.core.metrics.Metrics;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.core.ServerContext;
import dan200.computercraft.shared.computer.metrics.basic.Aggregate;
import dan200.computercraft.shared.computer.metrics.basic.AggregatedMetric;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import net.minecraft.server.MinecraftServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import javax.management.*;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.LongSupplier;
/**
* An MBean which exposes aggregate statistics about all computers on the server.
*/
public final class ComputerMBean implements DynamicMBean, ComputerMetricsObserver
{
private static final Logger LOGGER = LoggerFactory.getLogger( ComputerMBean.class );
private static @Nullable ComputerMBean instance;
private final Map<String, LongSupplier> attributes = new HashMap<>();
private final Int2ObjectMap<Counter> values = new Int2ObjectOpenHashMap<>();
private final MBeanInfo info;
private ComputerMBean()
{
Metrics.init();
List<MBeanAttributeInfo> attributes = new ArrayList<>();
for( Map.Entry<String, Metric> field : Metric.metrics().entrySet() )
{
String name = CaseFormat.LOWER_UNDERSCORE.to( CaseFormat.LOWER_CAMEL, field.getKey() );
add( name, field.getValue(), attributes );
}
info = new MBeanInfo( ComputerMBean.class.getSimpleName(), "metrics about all computers on the server", attributes.toArray( new MBeanAttributeInfo[0] ), null, null, null );
}
public static void register()
{
if( instance != null ) return;
try
{
ManagementFactory.getPlatformMBeanServer().registerMBean( instance = new ComputerMBean(), new ObjectName( "dan200.computercraft:type=Computers" ) );
}
catch( InstanceAlreadyExistsException | MBeanRegistrationException | NotCompliantMBeanException |
MalformedObjectNameException e )
{
LOGGER.warn( "Failed to register JMX bean", e );
}
}
public static void start( MinecraftServer server )
{
if( instance != null ) ServerContext.get( server ).metrics().addObserver( instance );
}
@Override
public Object getAttribute( String attribute ) throws AttributeNotFoundException
{
LongSupplier value = attributes.get( attribute );
if( value == null ) throw new AttributeNotFoundException();
return value.getAsLong();
}
@Override
public void setAttribute( Attribute attribute ) throws InvalidAttributeValueException
{
throw new InvalidAttributeValueException( "Cannot set attribute" );
}
@Override
public AttributeList getAttributes( String[] names )
{
AttributeList result = new AttributeList( names.length );
for( String name : names )
{
LongSupplier value = attributes.get( name );
if( value != null ) result.add( new Attribute( name, value.getAsLong() ) );
}
return result;
}
@Override
public AttributeList setAttributes( AttributeList attributes )
{
return new AttributeList();
}
@Nullable
@Override
public Object invoke( String actionName, Object[] params, String[] signature )
{
return null;
}
@Override
public MBeanInfo getMBeanInfo()
{
return info;
}
private void observe( Metric field, long change )
{
Counter counter = values.get( field.id() );
counter.value.addAndGet( change );
counter.count.incrementAndGet();
}
@Override
public void observe( ServerComputer computer, Metric.Counter counter )
{
observe( counter, 1 );
}
@Override
public void observe( ServerComputer computer, Metric.Event event, long value )
{
observe( event, value );
}
private MBeanAttributeInfo addAttribute( String name, String description, LongSupplier value )
{
attributes.put( name, value );
return new MBeanAttributeInfo( name, "long", description, true, false, false );
}
private void add( String name, Metric field, List<MBeanAttributeInfo> attributes )
{
Counter counter = new Counter();
values.put( field.id(), counter );
String prettyName = new AggregatedMetric( field, Aggregate.NONE ).displayName().getString();
attributes.add( addAttribute( name, prettyName, counter.value::longValue ) );
if( field instanceof Metric.Event )
{
String countName = new AggregatedMetric( field, Aggregate.COUNT ).displayName().getString();
attributes.add( addAttribute( name + "Count", countName, counter.count::longValue ) );
}
}
private static class Counter
{
final AtomicLong value = new AtomicLong();
final AtomicLong count = new AtomicLong();
}
}

View File

@ -0,0 +1,35 @@
/*
* 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.shared.computer.metrics;
import dan200.computercraft.core.metrics.Metric;
import dan200.computercraft.core.metrics.MetricsObserver;
import dan200.computercraft.shared.computer.core.ServerComputer;
/**
* A global version of {@link MetricsObserver}, which monitors multiple computers.
*/
public interface ComputerMetricsObserver
{
/**
* Increment a computer's counter by 1.
*
* @param computer The computer which incremented its counter.
* @param counter The counter to observe.
* @see MetricsObserver#observe(Metric.Counter)
*/
void observe( ServerComputer computer, Metric.Counter counter );
/**
* Observe a single instance of an event.
*
* @param computer The computer which incremented its counter.
* @param event The event to observe.
* @param value The value corresponding to this event.
* @see MetricsObserver#observe(Metric.Event, long)
*/
void observe( ServerComputer computer, Metric.Event event, long value );
}

View File

@ -0,0 +1,121 @@
/*
* 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.shared.computer.metrics;
import dan200.computercraft.core.metrics.Metric;
import dan200.computercraft.core.metrics.MetricsObserver;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.core.ServerContext;
import dan200.computercraft.shared.computer.metrics.basic.BasicComputerMetricsObserver;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
/**
* The global metrics system.
*
* @see ServerContext#metrics() To obtain an instance of this system.
*/
public final class GlobalMetrics
{
volatile boolean enabled = false;
final Object lock = new Object();
final List<ComputerMetricsObserver> trackers = new ArrayList<>();
private final HashMap<UUID, BasicComputerMetricsObserver> instances = new HashMap<>();
/**
* Get a metrics observer for a specific player. This will not be active until
* {@link BasicComputerMetricsObserver#start()} is called.
*
* @param uuid The player's UUID.
* @return The metrics instance for this player.
*/
public BasicComputerMetricsObserver getMetricsInstance( UUID uuid )
{
synchronized( lock )
{
BasicComputerMetricsObserver context = instances.get( uuid );
if( context == null ) instances.put( uuid, context = new BasicComputerMetricsObserver( this ) );
return context;
}
}
/**
* Add a new global metrics observer. This will receive metrics data for all computers.
*
* @param tracker The observer to add.
*/
public void addObserver( ComputerMetricsObserver tracker )
{
synchronized( lock )
{
if( trackers.contains( tracker ) ) return;
trackers.add( tracker );
enabled = true;
}
}
/**
* Remove a previously-registered global metrics observer.
*
* @param tracker The observer to add.
*/
public void removeObserver( ComputerMetricsObserver tracker )
{
synchronized( lock )
{
trackers.remove( tracker );
enabled = !trackers.isEmpty();
}
}
/**
* Create an observer for a computer. This will delegate to all registered {@link ComputerMetricsObserver}s.
*
* @param computer The computer to create the observer for.
* @return The instantiated observer.
*/
public MetricsObserver createMetricObserver( ServerComputer computer )
{
return new DispatchObserver( computer );
}
private final class DispatchObserver implements MetricsObserver
{
private final ServerComputer computer;
private DispatchObserver( ServerComputer computer )
{
this.computer = computer;
}
@Override
public void observe( Metric.Counter counter )
{
if( !enabled ) return;
synchronized( lock )
{
// TODO: The lock here is nasty and aggressive. However, in my benchmarks I've found it has about
// equivalent performance to a CoW list and atomics. Would be good to drill into this, as locks do not
// scale well.
for( ComputerMetricsObserver observer : trackers ) observer.observe( computer, counter );
}
}
@Override
public void observe( Metric.Event event, long value )
{
if( !enabled ) return;
synchronized( lock )
{
for( ComputerMetricsObserver observer : trackers ) observer.observe( computer, event, value );
}
}
}
}

View File

@ -0,0 +1,33 @@
/*
* 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.shared.computer.metrics.basic;
import dan200.computercraft.core.metrics.Metric;
/**
* An aggregate over a {@link Metric}.
* <p>
* Only {@link Metric.Event events} support non-{@link Aggregate#NONE} aggregates.
*/
public enum Aggregate
{
NONE( "none" ),
COUNT( "count" ),
AVG( "avg" ),
MAX( "max" );
private final String id;
Aggregate( String id )
{
this.id = id;
}
public String id()
{
return id;
}
}

View File

@ -0,0 +1,62 @@
/*
* 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.shared.computer.metrics.basic;
import dan200.computercraft.core.metrics.Metric;
import dan200.computercraft.core.metrics.Metrics;
import net.minecraft.util.text.ITextComponent;
import net.minecraft.util.text.TranslationTextComponent;
import java.util.Arrays;
import java.util.stream.Stream;
/**
* An aggregate of a specific metric.
*/
public class AggregatedMetric
{
private static final String TRANSLATION_PREFIX = "tracking_field.computercraft.";
private final Metric metric;
private final Aggregate aggregate;
public AggregatedMetric( Metric metric, Aggregate aggregate )
{
this.metric = metric;
this.aggregate = aggregate;
}
public Metric metric()
{
return metric;
}
public Aggregate aggregate()
{
return aggregate;
}
public static Stream<AggregatedMetric> aggregatedMetrics()
{
Metrics.init();
return Metric.metrics().values().stream()
.flatMap( m -> m instanceof Metric.Counter
? Stream.of( new AggregatedMetric( m, Aggregate.NONE ) )
: Arrays.stream( Aggregate.values() ).map( a -> new AggregatedMetric( m, a ) )
);
}
public String name()
{
return aggregate() == Aggregate.NONE ? metric.name() : metric().name() + "_" + aggregate().id();
}
public ITextComponent displayName()
{
TranslationTextComponent name = new TranslationTextComponent( TRANSLATION_PREFIX + metric().name() + ".name" );
return aggregate() == Aggregate.NONE ? name : new TranslationTextComponent( TRANSLATION_PREFIX + aggregate().id(), name );
}
}

View File

@ -0,0 +1,89 @@
/*
* 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.shared.computer.metrics.basic;
import com.google.common.collect.MapMaker;
import dan200.computercraft.core.metrics.Metric;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.metrics.ComputerMetricsObserver;
import dan200.computercraft.shared.computer.metrics.GlobalMetrics;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Tracks timing information about computers, including how long they ran for and the number of events they handled.
* <p>
* Note that this will retain timings for computers which have been deleted.
*/
public class BasicComputerMetricsObserver implements ComputerMetricsObserver
{
private final GlobalMetrics owner;
private boolean tracking = false;
private final List<ComputerMetrics> timings = new ArrayList<>();
private final Map<ServerComputer, ComputerMetrics> timingLookup = new MapMaker().weakKeys().makeMap();
public BasicComputerMetricsObserver( GlobalMetrics owner )
{
this.owner = owner;
}
public synchronized void start()
{
if( !tracking ) owner.addObserver( this );
tracking = true;
timings.clear();
timingLookup.clear();
}
public synchronized boolean stop()
{
if( !tracking ) return false;
owner.removeObserver( this );
tracking = false;
timingLookup.clear();
return true;
}
public synchronized List<ComputerMetrics> getSnapshot()
{
ArrayList<ComputerMetrics> timings = new ArrayList<>( this.timings.size() );
for( ComputerMetrics timing : this.timings ) timings.add( new ComputerMetrics( timing ) );
return timings;
}
public synchronized List<ComputerMetrics> getTimings()
{
return new ArrayList<>( timings );
}
private ComputerMetrics getMetrics( ServerComputer computer )
{
ComputerMetrics existing = timingLookup.get( computer );
if( existing != null ) return existing;
ComputerMetrics metrics = new ComputerMetrics( computer );
timingLookup.put( computer, metrics );
timings.add( metrics );
return metrics;
}
@Override
public synchronized void observe( ServerComputer computer, Metric.Counter counter )
{
getMetrics( computer ).observe( counter );
}
@Override
public synchronized void observe( ServerComputer computer, Metric.Event event, long value )
{
getMetrics( computer ).observe( event, value );
}
}

View File

@ -0,0 +1,129 @@
/*
* 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.shared.computer.metrics.basic;
import dan200.computercraft.core.metrics.Metric;
import dan200.computercraft.shared.computer.core.ServerComputer;
import javax.annotation.Nullable;
import java.lang.ref.WeakReference;
import java.util.Arrays;
/**
* Metrics for an individual computer.
*/
public final class ComputerMetrics
{
private static final int DEFAULT_LEN = 16;
private final WeakReference<ServerComputer> computer;
private final int computerId;
private long[] counts;
private long[] totals;
private long[] max;
ComputerMetrics( ServerComputer computer )
{
this.computer = new WeakReference<>( computer );
computerId = computer.getID();
counts = new long[DEFAULT_LEN];
totals = new long[DEFAULT_LEN];
max = new long[DEFAULT_LEN];
}
ComputerMetrics( ComputerMetrics other )
{
computer = other.computer;
computerId = other.computerId;
counts = Arrays.copyOf( other.counts, other.counts.length );
totals = Arrays.copyOf( other.totals, other.totals.length );
max = Arrays.copyOf( other.max, other.max.length );
}
@Nullable
public ServerComputer computer()
{
return computer.get();
}
public int computerId()
{
return computerId;
}
private static long get( long[] values, Metric metric )
{
return metric.id() >= values.length ? 0 : values[metric.id()];
}
private long avg( long total, long count )
{
return count == 0 ? 0 : total / count;
}
public long get( Metric metric, Aggregate aggregate )
{
if( metric instanceof Metric.Counter ) return get( counts, metric );
if( metric instanceof Metric.Event )
{
switch( aggregate )
{
case NONE:
return get( totals, metric );
case COUNT:
return get( counts, metric );
case AVG:
return avg( get( totals, metric ), get( counts, metric ) );
case MAX:
return get( max, metric );
default:
throw new IllegalArgumentException();
}
}
throw new IllegalArgumentException( "Unknown metric " + metric.name() );
}
public String getFormatted( Metric field, Aggregate aggregate )
{
long value = get( field, aggregate );
switch( aggregate )
{
case COUNT:
return Metric.formatDefault( value );
case AVG:
case MAX:
case NONE:
return field.format( value );
default:
throw new IllegalArgumentException();
}
}
private void ensureCapacity( Metric metric )
{
if( metric.id() < counts.length ) return;
int newCapacity = Math.max( metric.id(), counts.length * 2 );
counts = Arrays.copyOf( counts, newCapacity );
totals = Arrays.copyOf( totals, newCapacity );
max = Arrays.copyOf( max, newCapacity );
}
void observe( Metric.Counter counter )
{
ensureCapacity( counter );
counts[counter.id()]++;
}
void observe( Metric.Event event, long value )
{
ensureCapacity( event );
counts[event.id()]++;
totals[event.id()] += value;
if( value > max[event.id()] ) max[event.id()] = value;
}
}

View File

@ -14,7 +14,7 @@
import dan200.computercraft.api.turtle.event.TurtleActionEvent;
import dan200.computercraft.api.turtle.event.TurtleInspectItemEvent;
import dan200.computercraft.core.apis.IAPIEnvironment;
import dan200.computercraft.core.tracking.TrackingField;
import dan200.computercraft.core.metrics.Metrics;
import dan200.computercraft.shared.peripheral.generic.data.ItemData;
import dan200.computercraft.shared.peripheral.generic.methods.InventoryMethods;
import dan200.computercraft.shared.turtle.core.*;
@ -93,7 +93,7 @@ public String[] getNames()
private MethodResult trackCommand( ITurtleCommand command )
{
environment.addTrackingChange( TrackingField.TURTLE_OPS );
environment.observe( Metrics.TURTLE_OPS );
return turtle.executeCommand( command );
}
@ -191,7 +191,7 @@ public final MethodResult turnRight()
@LuaFunction
public final MethodResult dig( Optional<TurtleSide> side )
{
environment.addTrackingChange( TrackingField.TURTLE_OPS );
environment.observe( Metrics.TURTLE_OPS );
return trackCommand( TurtleToolCommand.dig( InteractDirection.FORWARD, side.orElse( null ) ) );
}
@ -207,7 +207,7 @@ public final MethodResult dig( Optional<TurtleSide> side )
@LuaFunction
public final MethodResult digUp( Optional<TurtleSide> side )
{
environment.addTrackingChange( TrackingField.TURTLE_OPS );
environment.observe( Metrics.TURTLE_OPS );
return trackCommand( TurtleToolCommand.dig( InteractDirection.UP, side.orElse( null ) ) );
}
@ -223,7 +223,7 @@ public final MethodResult digUp( Optional<TurtleSide> side )
@LuaFunction
public final MethodResult digDown( Optional<TurtleSide> side )
{
environment.addTrackingChange( TrackingField.TURTLE_OPS );
environment.observe( Metrics.TURTLE_OPS );
return trackCommand( TurtleToolCommand.dig( InteractDirection.DOWN, side.orElse( null ) ) );
}

View File

@ -92,12 +92,8 @@
"argument.computercraft.computer.many_matching": "Multiple computers matching '%s' (instances %s)",
"argument.computercraft.tracking_field.no_field": "Unknown field '%s'",
"argument.computercraft.argument_expected": "Argument expected",
"tracking_field.computercraft.tasks.name": "Tasks",
"tracking_field.computercraft.total.name": "Total time",
"tracking_field.computercraft.average.name": "Average time",
"tracking_field.computercraft.max.name": "Max time",
"tracking_field.computercraft.server_count.name": "Server task count",
"tracking_field.computercraft.server_time.name": "Server task time",
"tracking_field.computercraft.computer_tasks.name": "Tasks",
"tracking_field.computercraft.server_tasks.name": "Server tasks",
"tracking_field.computercraft.peripheral.name": "Peripheral calls",
"tracking_field.computercraft.fs.name": "Filesystem operations",
"tracking_field.computercraft.turtle.name": "Turtle operations",
@ -108,6 +104,9 @@
"tracking_field.computercraft.websocket_outgoing.name": "Websocket outgoing",
"tracking_field.computercraft.coroutines_created.name": "Coroutines created",
"tracking_field.computercraft.coroutines_dead.name": "Coroutines disposed",
"tracking_field.computercraft.max": "%s (max)",
"tracking_field.computercraft.avg": "%s (avg)",
"tracking_field.computercraft.count": "%s (count)",
"gui.computercraft.tooltip.copy": "Copy to clipboard",
"gui.computercraft.tooltip.computer_id": "Computer ID: %s",
"gui.computercraft.tooltip.disk_id": "Disk ID: %s",

View File

@ -10,8 +10,8 @@
import dan200.computercraft.core.computer.ComputerSide
import dan200.computercraft.core.computer.GlobalEnvironment
import dan200.computercraft.core.filesystem.FileSystem
import dan200.computercraft.core.metrics.Metric
import dan200.computercraft.core.terminal.Terminal
import dan200.computercraft.core.tracking.TrackingField
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
@ -43,7 +43,8 @@ override fun getLabel(): String? = null
override fun setLabel(label: String?) {}
override fun startTimer(ticks: Long): Int = 0
override fun cancelTimer(id: Int) {}
override fun addTrackingChange(field: TrackingField, change: Long) {}
override fun observe(field: Metric.Counter) {}
override fun observe(field: Metric.Event, change: Long) {}
}
class EventResult(val name: String, val args: Array<Any?>)

View File

@ -11,6 +11,8 @@
import dan200.computercraft.core.filesystem.FileMount;
import dan200.computercraft.core.filesystem.JarMount;
import dan200.computercraft.core.filesystem.MemoryMount;
import dan200.computercraft.core.metrics.Metric;
import dan200.computercraft.core.metrics.MetricsObserver;
import javax.annotation.Nonnull;
import java.io.File;
@ -24,7 +26,7 @@
/**
* A very basic environment.
*/
public class BasicEnvironment implements ComputerEnvironment, GlobalEnvironment
public class BasicEnvironment implements ComputerEnvironment, GlobalEnvironment, MetricsObserver
{
private final IWritableMount mount;
@ -56,6 +58,12 @@ public double getTimeOfDay()
return 0;
}
@Override
public MetricsObserver getMetrics()
{
return this;
}
@Nonnull
@Override
public String getHostString()
@ -144,4 +152,14 @@ private static File getContainingFile( Class<?> klass )
return new File( url.getPath() );
}
}
@Override
public void observe( Metric.Counter counter )
{
}
@Override
public void observe( Metric.Event event, long value )
{
}
}

View File

@ -25,7 +25,7 @@
/**
* Creates "fake" computers, which just run user-defined tasks rather than Lua code.
*
* <p>
* Note, this will clobber some parts of the global state. It's recommended you use this inside an {@link IsolatedRunner}.
*/
public class FakeComputerManager
@ -43,7 +43,7 @@ interface Task
static
{
ComputerExecutor.luaFactory = ( computer, timeout ) -> new DummyLuaMachine( timeout, machines.get( computer ) );
ComputerExecutor.luaFactory = args -> new DummyLuaMachine( args.timeout );
}
/**
@ -57,7 +57,9 @@ public static Computer create()
{
BasicEnvironment environment = new BasicEnvironment();
Computer computer = new Computer( environment, environment, new Terminal( 51, 19, true ), 0 );
machines.put( computer, new ConcurrentLinkedQueue<>() );
ConcurrentLinkedQueue<Task> tasks = new ConcurrentLinkedQueue<>();
computer.addApi( new QueuePassingAPI( tasks ) );
machines.put( computer, tasks );
return computer;
}
@ -158,20 +160,36 @@ private static <T extends Throwable> void rethrow( Throwable e ) throws T
throw (T) e;
}
private static final class QueuePassingAPI implements ILuaAPI
{
final Queue<Task> tasks;
private QueuePassingAPI( Queue<Task> tasks )
{
this.tasks = tasks;
}
@Override
public String[] getNames()
{
return new String[0];
}
}
private static class DummyLuaMachine implements ILuaMachine
{
private final TimeoutState state;
private final Queue<Task> handleEvent;
private @javax.annotation.Nullable Queue<Task> tasks;
DummyLuaMachine( TimeoutState state, Queue<Task> handleEvent )
DummyLuaMachine( TimeoutState state )
{
this.state = state;
this.handleEvent = handleEvent;
}
@Override
public void addAPI( @Nonnull ILuaAPI api )
{
if( api instanceof QueuePassingAPI ) tasks = ((QueuePassingAPI) api).tasks;
}
@Override
@ -185,7 +203,8 @@ public MachineResult handleEvent( @Nullable String eventName, @Nullable Object[]
{
try
{
return handleEvent.remove().run( state );
if( tasks == null ) throw new IllegalStateException( "Not received tasks yet" );
return tasks.remove().run( state );
}
catch( Throwable e )
{