diff --git a/src/main/java/dan200/computercraft/core/apis/FSAPI.java b/src/main/java/dan200/computercraft/core/apis/FSAPI.java index 12d84fb75..c1719b68f 100644 --- a/src/main/java/dan200/computercraft/core/apis/FSAPI.java +++ b/src/main/java/dan200/computercraft/core/apis/FSAPI.java @@ -16,7 +16,7 @@ import dan200.computercraft.core.apis.handles.EncodedWritableHandle; 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 class FSAPI implements ILuaAPI @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 class FSAPI implements ILuaAPI { try { - environment.addTrackingChange( TrackingField.FS_OPS ); + environment.observe( Metrics.FS_OPS ); fileSystem.makeDir( path ); } catch( FileSystemException e ) @@ -299,7 +299,7 @@ public class FSAPI implements ILuaAPI { try { - environment.addTrackingChange( TrackingField.FS_OPS ); + environment.observe( Metrics.FS_OPS ); fileSystem.move( path, dest ); } catch( FileSystemException e ) @@ -322,7 +322,7 @@ public class FSAPI implements ILuaAPI { try { - environment.addTrackingChange( TrackingField.FS_OPS ); + environment.observe( Metrics.FS_OPS ); fileSystem.copy( path, dest ); } catch( FileSystemException e ) @@ -345,7 +345,7 @@ public class FSAPI implements ILuaAPI { try { - environment.addTrackingChange( TrackingField.FS_OPS ); + environment.observe( Metrics.FS_OPS ); fileSystem.delete( path ); } catch( FileSystemException e ) @@ -411,7 +411,7 @@ public class FSAPI implements ILuaAPI @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 class FSAPI implements ILuaAPI { try { - environment.addTrackingChange( TrackingField.FS_OPS ); + environment.observe( Metrics.FS_OPS ); return fileSystem.find( path ); } catch( FileSystemException e ) diff --git a/src/main/java/dan200/computercraft/core/apis/IAPIEnvironment.java b/src/main/java/dan200/computercraft/core/apis/IAPIEnvironment.java index 796c63b9f..47c6de695 100644 --- a/src/main/java/dan200/computercraft/core/apis/IAPIEnvironment.java +++ b/src/main/java/dan200/computercraft/core/apis/IAPIEnvironment.java @@ -11,8 +11,8 @@ import dan200.computercraft.core.computer.ComputerEnvironment; 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 @@ public interface IAPIEnvironment 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 ); } diff --git a/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java b/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java index 79d916173..09579a74d 100644 --- a/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java +++ b/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java @@ -16,7 +16,7 @@ import dan200.computercraft.core.asm.LuaMethod; 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 class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChange 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 ); } diff --git a/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequest.java b/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequest.java index 255b72307..1d3695946 100644 --- a/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequest.java +++ b/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequest.java @@ -12,7 +12,7 @@ import dan200.computercraft.core.apis.http.NetworkUtils; 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 @@ public class HttpRequest extends Resource } // 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() diff --git a/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequestHandler.java b/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequestHandler.java index db034de8b..8a6e66fc0 100644 --- a/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequestHandler.java +++ b/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequestHandler.java @@ -13,7 +13,7 @@ import dan200.computercraft.core.apis.handles.HandleGeneric; 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 @@ public final class HttpRequestHandler extends SimpleChannelInboundHandler { 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 ) diff --git a/src/main/java/dan200/computercraft/core/computer/Computer.java b/src/main/java/dan200/computercraft/core/computer/Computer.java index d0c7c2bec..e897b25b1 100644 --- a/src/main/java/dan200/computercraft/core/computer/Computer.java +++ b/src/main/java/dan200/computercraft/core/computer/Computer.java @@ -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 class Computer 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() diff --git a/src/main/java/dan200/computercraft/core/computer/ComputerEnvironment.java b/src/main/java/dan200/computercraft/core/computer/ComputerEnvironment.java index 498a211e9..4f7649dc9 100644 --- a/src/main/java/dan200/computercraft/core/computer/ComputerEnvironment.java +++ b/src/main/java/dan200/computercraft/core/computer/ComputerEnvironment.java @@ -7,6 +7,7 @@ package dan200.computercraft.core.computer; 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. * diff --git a/src/main/java/dan200/computercraft/core/computer/ComputerExecutor.java b/src/main/java/dan200/computercraft/core/computer/ComputerExecutor.java index 83f29732e..5ee8eebed 100644 --- a/src/main/java/dan200/computercraft/core/computer/ComputerExecutor.java +++ b/src/main/java/dan200/computercraft/core/computer/ComputerExecutor.java @@ -15,9 +15,11 @@ import dan200.computercraft.core.filesystem.FileSystem; 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 apis = new ArrayList<>(); final TimeoutState timeout = new TimeoutState(); @@ -157,12 +161,14 @@ final class ComputerExecutor */ final AtomicReference 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 @@ final class ComputerExecutor private IWritableMount getRootMount() { - if( rootMount == null ) rootMount = computer.getComputerEnvironment().createRootMount(); + if( rootMount == null ) rootMount = computerEnvironment.createRootMount(); return rootMount; } @@ -395,7 +401,9 @@ final class ComputerExecutor } // 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 @@ final class ComputerExecutor timeout.stopTimer(); } - Tracking.addTaskTiming( getComputer(), timeout.nanoCurrent() ); + metrics.observe( Metrics.COMPUTER_TASKS, timeout.nanoCurrent() ); if( interruptedEvent ) return true; diff --git a/src/main/java/dan200/computercraft/core/computer/Environment.java b/src/main/java/dan200/computercraft/core/computer/Environment.java index cefbc9358..18a8a0595 100644 --- a/src/main/java/dan200/computercraft/core/computer/Environment.java +++ b/src/main/java/dan200/computercraft/core/computer/Environment.java @@ -10,9 +10,9 @@ import dan200.computercraft.api.peripheral.IPeripheral; 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 @@ import java.util.Iterator; 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 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 final class Environment implements IAPIEnvironment @Override public ComputerEnvironment getComputerEnvironment() { - return computer.getComputerEnvironment(); + return environment; } @Nonnull @@ -367,9 +371,15 @@ public final class Environment implements IAPIEnvironment } @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 diff --git a/src/main/java/dan200/computercraft/core/lua/LuaContext.java b/src/main/java/dan200/computercraft/core/computer/LuaContext.java similarity index 93% rename from src/main/java/dan200/computercraft/core/lua/LuaContext.java rename to src/main/java/dan200/computercraft/core/computer/LuaContext.java index bd058a921..e8b45f7ef 100644 --- a/src/main/java/dan200/computercraft/core/lua/LuaContext.java +++ b/src/main/java/dan200/computercraft/core/computer/LuaContext.java @@ -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; diff --git a/src/main/java/dan200/computercraft/core/computer/MainThreadExecutor.java b/src/main/java/dan200/computercraft/core/computer/MainThreadExecutor.java index 6169b4adc..e37e77367 100644 --- a/src/main/java/dan200/computercraft/core/computer/MainThreadExecutor.java +++ b/src/main/java/dan200/computercraft/core/computer/MainThreadExecutor.java @@ -7,7 +7,8 @@ package dan200.computercraft.core.computer; 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 @@ final class MainThreadExecutor implements IWorkMonitor 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. diff --git a/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java b/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java index 8a0edc1ed..4c2bdf85c 100644 --- a/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java +++ b/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java @@ -12,10 +12,9 @@ import dan200.computercraft.api.lua.ILuaContext; 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 class CobaltLuaMachine implements ILuaMachine } finally { - Tracking.addValue( this.computer, TrackingField.COROUTINES_DISPOSED, 1 ); + metrics.observe( Metrics.COROUTINES_DISPOSED ); } } ); } ) @@ -110,7 +108,7 @@ public class CobaltLuaMachine implements ILuaMachine // 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 ) { diff --git a/src/main/java/dan200/computercraft/core/lua/ILuaMachine.java b/src/main/java/dan200/computercraft/core/lua/ILuaMachine.java index 2cfa48f3b..45f0e2ea8 100644 --- a/src/main/java/dan200/computercraft/core/lua/ILuaMachine.java +++ b/src/main/java/dan200/computercraft/core/lua/ILuaMachine.java @@ -7,8 +7,6 @@ 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; @@ -77,6 +75,6 @@ public interface ILuaMachine interface Factory { - ILuaMachine create( Computer computer, TimeoutState timeout ); + ILuaMachine create( MachineEnvironment environment ); } } diff --git a/src/main/java/dan200/computercraft/core/lua/MachineEnvironment.java b/src/main/java/dan200/computercraft/core/lua/MachineEnvironment.java new file mode 100644 index 000000000..2462c3886 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/lua/MachineEnvironment.java @@ -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; + } +} diff --git a/src/main/java/dan200/computercraft/core/metrics/Metric.java b/src/main/java/dan200/computercraft/core/metrics/Metric.java new file mode 100644 index 000000000..b7cc0f8f5 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/metrics/Metric.java @@ -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}. + *

+ * 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 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 format; + + private Metric( String name, String unit, LongFunction 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. + *

+ * 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 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 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 ) ); + } +} diff --git a/src/main/java/dan200/computercraft/core/metrics/Metrics.java b/src/main/java/dan200/computercraft/core/metrics/Metrics.java new file mode 100644 index 000000000..6f9d1510f --- /dev/null +++ b/src/main/java/dan200/computercraft/core/metrics/Metrics.java @@ -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() + { + } +} diff --git a/src/main/java/dan200/computercraft/core/metrics/MetricsObserver.java b/src/main/java/dan200/computercraft/core/metrics/MetricsObserver.java new file mode 100644 index 000000000..8464238c2 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/metrics/MetricsObserver.java @@ -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. + *

+ * 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 ); +} diff --git a/src/main/java/dan200/computercraft/core/tracking/ComputerMBean.java b/src/main/java/dan200/computercraft/core/tracking/ComputerMBean.java deleted file mode 100644 index 8d1fe8acc..000000000 --- a/src/main/java/dan200/computercraft/core/tracking/ComputerMBean.java +++ /dev/null @@ -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 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 attributes = new HashMap<>(); - private final Map values = new HashMap<>(); - private final MBeanInfo info; - - private ComputerMBean() - { - List attributes = new ArrayList<>(); - for( Map.Entry 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 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(); - } -} diff --git a/src/main/java/dan200/computercraft/core/tracking/ComputerTracker.java b/src/main/java/dan200/computercraft/core/tracking/ComputerTracker.java deleted file mode 100644 index 865d53150..000000000 --- a/src/main/java/dan200/computercraft/core/tracking/ComputerTracker.java +++ /dev/null @@ -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; - private final int computerId; - - private long tasks; - private long totalTime; - private long maxTime; - - private long serverCount; - private long serverTime; - - private final Object2LongOpenHashMap 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 ) ); - } -} diff --git a/src/main/java/dan200/computercraft/core/tracking/Tracker.java b/src/main/java/dan200/computercraft/core/tracking/Tracker.java deleted file mode 100644 index 56114f19f..000000000 --- a/src/main/java/dan200/computercraft/core/tracking/Tracker.java +++ /dev/null @@ -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 ) - { - } -} diff --git a/src/main/java/dan200/computercraft/core/tracking/Tracking.java b/src/main/java/dan200/computercraft/core/tracking/Tracking.java deleted file mode 100644 index 741982bad..000000000 --- a/src/main/java/dan200/computercraft/core/tracking/Tracking.java +++ /dev/null @@ -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 contexts = new HashMap<>(); - private static final List 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 ); - } - } -} diff --git a/src/main/java/dan200/computercraft/core/tracking/TrackingContext.java b/src/main/java/dan200/computercraft/core/tracking/TrackingContext.java deleted file mode 100644 index e4e76ce21..000000000 --- a/src/main/java/dan200/computercraft/core/tracking/TrackingContext.java +++ /dev/null @@ -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 will 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 timings = new ArrayList<>(); - private final Map 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 getImmutableTimings() - { - ArrayList timings = new ArrayList<>( this.timings.size() ); - for( ComputerTracker timing : this.timings ) timings.add( new ComputerTracker( timing ) ); - return timings; - } - - public synchronized List 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 ); - } - } -} diff --git a/src/main/java/dan200/computercraft/core/tracking/TrackingField.java b/src/main/java/dan200/computercraft/core/tracking/TrackingField.java deleted file mode 100644 index ec15181f5..000000000 --- a/src/main/java/dan200/computercraft/core/tracking/TrackingField.java +++ /dev/null @@ -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 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 format; - - public String id() - { - return id; - } - - public String translationKey() - { - return translationKey; - } - - private TrackingField( String id, LongFunction 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 format ) - { - TrackingField field = new TrackingField( id, format ); - fields.put( id, field ); - return field; - } - - public static Map 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 ) ); - } -} diff --git a/src/main/java/dan200/computercraft/shared/CommonHooks.java b/src/main/java/dan200/computercraft/shared/CommonHooks.java index bde2767a7..a9c5547b5 100644 --- a/src/main/java/dan200/computercraft/shared/CommonHooks.java +++ b/src/main/java/dan200/computercraft/shared/CommonHooks.java @@ -9,10 +9,9 @@ import dan200.computercraft.ComputerCraft; import dan200.computercraft.core.apis.http.NetworkUtils; import dan200.computercraft.core.computer.MainThread; import dan200.computercraft.core.filesystem.ResourceMount; -import dan200.computercraft.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 final class CommonHooks resetState(); ServerContext.create( server ); - ComputerMBean.registerTracker(); + ComputerMBean.start( server ); } @SubscribeEvent @@ -88,7 +87,6 @@ public final class CommonHooks ServerContext.close(); MainThread.reset(); WirelessNetwork.resetNetworks(); - Tracking.reset(); NetworkUtils.reset(); } diff --git a/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java b/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java index e021d35fe..c25a99152 100644 --- a/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java +++ b/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java @@ -9,17 +9,17 @@ import com.mojang.brigadier.CommandDispatcher; 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.Exceptions.*; 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 final class CommandComputerCraft .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 final class CommandComputerCraft .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 @@ public final class CommandComputerCraft } @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 DEFAULT_FIELDS = Arrays.asList( TrackingField.TASKS, TrackingField.TOTAL_TIME, TrackingField.AVERAGE_TIME, TrackingField.MAX_TIME ); + private static final List 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 fields ) throws CommandSyntaxException + private static int displayTimings( CommandSource source, AggregatedMetric sortField, List 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 timings, @Nonnull TrackingField sortField, @Nonnull List fields ) throws CommandSyntaxException + private static int displayTimings( CommandSource source, List timings, AggregatedMetric sortField, List fields ) throws CommandSyntaxException { if( timings.isEmpty() ) throw NO_TIMINGS_EXCEPTION.create(); - Map 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.comparing( x -> x.get( sortField ) ).reversed() ); + timings.sort( Comparator.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 ); } diff --git a/src/main/java/dan200/computercraft/shared/command/arguments/ArgumentSerializers.java b/src/main/java/dan200/computercraft/shared/command/arguments/ArgumentSerializers.java index ee9486186..998bdf725 100644 --- a/src/main/java/dan200/computercraft/shared/command/arguments/ArgumentSerializers.java +++ b/src/main/java/dan200/computercraft/shared/command/arguments/ArgumentSerializers.java @@ -32,7 +32,7 @@ public final class ArgumentSerializers 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() ); diff --git a/src/main/java/dan200/computercraft/shared/command/arguments/TrackingFieldArgumentType.java b/src/main/java/dan200/computercraft/shared/command/arguments/TrackingFieldArgumentType.java index daf76edba..b683703fb 100644 --- a/src/main/java/dan200/computercraft/shared/command/arguments/TrackingFieldArgumentType.java +++ b/src/main/java/dan200/computercraft/shared/command/arguments/TrackingFieldArgumentType.java @@ -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 +public final class TrackingFieldArgumentType extends ChoiceArgumentType { 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; } diff --git a/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java b/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java index 4586df3b0..3344fe541 100644 --- a/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java +++ b/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java @@ -14,6 +14,7 @@ import dan200.computercraft.core.apis.IAPIEnvironment; 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 class ServerComputer implements InputHandler, ComputerEnvironment 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 class ServerComputer implements InputHandler, ComputerEnvironment computer.setLabel( label ); } - // IComputerEnvironment implementation - @Override public double getTimeOfDay() { @@ -276,6 +277,12 @@ public class ServerComputer implements InputHandler, ComputerEnvironment return (int) ((world.getDayTime() + 6000) / 24000) + 1; } + @Override + public MetricsObserver getMetrics() + { + return metrics; + } + @Override public IWritableMount createRootMount() { diff --git a/src/main/java/dan200/computercraft/shared/computer/core/ServerContext.java b/src/main/java/dan200/computercraft/shared/computer/core/ServerContext.java index c8f1dcc22..63f7d3486 100644 --- a/src/main/java/dan200/computercraft/shared/computer/core/ServerContext.java +++ b/src/main/java/dan200/computercraft/shared/computer/core/ServerContext.java @@ -11,6 +11,7 @@ import dan200.computercraft.api.ComputerCraftAPI; 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 final class ServerContext 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; diff --git a/src/main/java/dan200/computercraft/shared/computer/metrics/ComputerMBean.java b/src/main/java/dan200/computercraft/shared/computer/metrics/ComputerMBean.java new file mode 100644 index 000000000..e4c3ec162 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/metrics/ComputerMBean.java @@ -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 attributes = new HashMap<>(); + private final Int2ObjectMap values = new Int2ObjectOpenHashMap<>(); + private final MBeanInfo info; + + private ComputerMBean() + { + Metrics.init(); + + List attributes = new ArrayList<>(); + for( Map.Entry 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 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(); + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/metrics/ComputerMetricsObserver.java b/src/main/java/dan200/computercraft/shared/computer/metrics/ComputerMetricsObserver.java new file mode 100644 index 000000000..5187788b6 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/metrics/ComputerMetricsObserver.java @@ -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 ); +} diff --git a/src/main/java/dan200/computercraft/shared/computer/metrics/GlobalMetrics.java b/src/main/java/dan200/computercraft/shared/computer/metrics/GlobalMetrics.java new file mode 100644 index 000000000..84dd66b9b --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/metrics/GlobalMetrics.java @@ -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 trackers = new ArrayList<>(); + + private final HashMap 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 ); + } + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/metrics/basic/Aggregate.java b/src/main/java/dan200/computercraft/shared/computer/metrics/basic/Aggregate.java new file mode 100644 index 000000000..22993ce2e --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/metrics/basic/Aggregate.java @@ -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}. + *

+ * 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; + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/metrics/basic/AggregatedMetric.java b/src/main/java/dan200/computercraft/shared/computer/metrics/basic/AggregatedMetric.java new file mode 100644 index 000000000..12f0ddb1c --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/metrics/basic/AggregatedMetric.java @@ -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 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 ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/metrics/basic/BasicComputerMetricsObserver.java b/src/main/java/dan200/computercraft/shared/computer/metrics/basic/BasicComputerMetricsObserver.java new file mode 100644 index 000000000..e065e6d9c --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/metrics/basic/BasicComputerMetricsObserver.java @@ -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. + *

+ * 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 timings = new ArrayList<>(); + private final Map 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 getSnapshot() + { + ArrayList timings = new ArrayList<>( this.timings.size() ); + for( ComputerMetrics timing : this.timings ) timings.add( new ComputerMetrics( timing ) ); + return timings; + } + + public synchronized List 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 ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/metrics/basic/ComputerMetrics.java b/src/main/java/dan200/computercraft/shared/computer/metrics/basic/ComputerMetrics.java new file mode 100644 index 000000000..747d10546 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/computer/metrics/basic/ComputerMetrics.java @@ -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 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; + } +} diff --git a/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java b/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java index acd571b5c..6f2bd23c0 100644 --- a/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java +++ b/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java @@ -14,7 +14,7 @@ import dan200.computercraft.api.turtle.TurtleSide; 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 class TurtleAPI implements ILuaAPI private MethodResult trackCommand( ITurtleCommand command ) { - environment.addTrackingChange( TrackingField.TURTLE_OPS ); + environment.observe( Metrics.TURTLE_OPS ); return turtle.executeCommand( command ); } @@ -191,7 +191,7 @@ public class TurtleAPI implements ILuaAPI @LuaFunction public final MethodResult dig( Optional 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 class TurtleAPI implements ILuaAPI @LuaFunction public final MethodResult digUp( Optional 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 class TurtleAPI implements ILuaAPI @LuaFunction public final MethodResult digDown( Optional side ) { - environment.addTrackingChange( TrackingField.TURTLE_OPS ); + environment.observe( Metrics.TURTLE_OPS ); return trackCommand( TurtleToolCommand.dig( InteractDirection.DOWN, side.orElse( null ) ) ); } diff --git a/src/main/resources/assets/computercraft/lang/en_us.json b/src/main/resources/assets/computercraft/lang/en_us.json index 60c468634..f40d1d927 100644 --- a/src/main/resources/assets/computercraft/lang/en_us.json +++ b/src/main/resources/assets/computercraft/lang/en_us.json @@ -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", diff --git a/src/test/java/dan200/computercraft/core/apis/AsyncRunner.kt b/src/test/java/dan200/computercraft/core/apis/AsyncRunner.kt index e85a6fe63..450241a2b 100644 --- a/src/test/java/dan200/computercraft/core/apis/AsyncRunner.kt +++ b/src/test/java/dan200/computercraft/core/apis/AsyncRunner.kt @@ -10,8 +10,8 @@ import dan200.computercraft.core.computer.ComputerEnvironment 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 @@ abstract class NullApiEnvironment : IAPIEnvironment { 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) diff --git a/src/test/java/dan200/computercraft/core/computer/BasicEnvironment.java b/src/test/java/dan200/computercraft/core/computer/BasicEnvironment.java index 759d12c45..e763f44b8 100644 --- a/src/test/java/dan200/computercraft/core/computer/BasicEnvironment.java +++ b/src/test/java/dan200/computercraft/core/computer/BasicEnvironment.java @@ -11,6 +11,8 @@ import dan200.computercraft.api.filesystem.IWritableMount; 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 @@ import java.net.URL; /** * 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 class BasicEnvironment implements ComputerEnvironment, GlobalEnvironment return 0; } + @Override + public MetricsObserver getMetrics() + { + return this; + } + @Nonnull @Override public String getHostString() @@ -144,4 +152,14 @@ public class BasicEnvironment implements ComputerEnvironment, GlobalEnvironment return new File( url.getPath() ); } } + + @Override + public void observe( Metric.Counter counter ) + { + } + + @Override + public void observe( Metric.Event event, long value ) + { + } } diff --git a/src/test/java/dan200/computercraft/core/computer/FakeComputerManager.java b/src/test/java/dan200/computercraft/core/computer/FakeComputerManager.java index b2f3bb168..a14d4fa9c 100644 --- a/src/test/java/dan200/computercraft/core/computer/FakeComputerManager.java +++ b/src/test/java/dan200/computercraft/core/computer/FakeComputerManager.java @@ -25,7 +25,7 @@ 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 @@ -43,7 +43,7 @@ public class FakeComputerManager static { - ComputerExecutor.luaFactory = ( computer, timeout ) -> new DummyLuaMachine( timeout, machines.get( computer ) ); + ComputerExecutor.luaFactory = args -> new DummyLuaMachine( args.timeout ); } /** @@ -57,7 +57,9 @@ public class FakeComputerManager { BasicEnvironment environment = new BasicEnvironment(); Computer computer = new Computer( environment, environment, new Terminal( 51, 19, true ), 0 ); - machines.put( computer, new ConcurrentLinkedQueue<>() ); + ConcurrentLinkedQueue tasks = new ConcurrentLinkedQueue<>(); + computer.addApi( new QueuePassingAPI( tasks ) ); + machines.put( computer, tasks ); return computer; } @@ -158,20 +160,36 @@ public class FakeComputerManager throw (T) e; } + private static final class QueuePassingAPI implements ILuaAPI + { + final Queue tasks; + + private QueuePassingAPI( Queue 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 handleEvent; + private @javax.annotation.Nullable Queue tasks; - DummyLuaMachine( TimeoutState state, Queue 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 class FakeComputerManager { try { - return handleEvent.remove().run( state ); + if( tasks == null ) throw new IllegalStateException( "Not received tasks yet" ); + return tasks.remove().run( state ); } catch( Throwable e ) {