From 0b26ab366d6f693047c7b8749374e4906d4c518c Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sat, 22 Oct 2022 01:35:04 +0100 Subject: [PATCH] Rewrite the metrics system - Remove TrackingField and replace it with a Metric abstract class. This has two concrete subclasses - Counter and Event. Events carry an additional piece of data each time it is observed, such as HTTP response size. - Computers now accept a MetricsObserver, which tracks metrics for this particular computer. This allows us to decouple Computer classes and metrics information. The concrete MetricsObserver class we use within Minecraft exposes the ServerComputer directly, so we no longer need to do the awkward mapping and lookups! - The /computercraft command can now do aggregates (count, avg, max) over all Event metrics. This removes the need for special handling of computer and server time. There's also a small number of changes in removing the coupling between Computer and some of its dependencies (ILuaMachine, MainThreadExecutor). This makes some future refactorings easier, I promise! --- .../dan200/computercraft/core/apis/FSAPI.java | 16 +- .../core/apis/IAPIEnvironment.java | 9 +- .../core/apis/PeripheralAPI.java | 4 +- .../core/apis/http/request/HttpRequest.java | 6 +- .../apis/http/request/HttpRequestHandler.java | 4 +- .../apis/http/websocket/WebsocketHandle.java | 4 +- .../apis/http/websocket/WebsocketHandler.java | 6 +- .../computercraft/core/computer/Computer.java | 14 +- .../core/computer/ComputerEnvironment.java | 9 + .../core/computer/ComputerExecutor.java | 18 +- .../core/computer/Environment.java | 22 ++- .../core/{lua => computer}/LuaContext.java | 4 +- .../core/computer/MainThreadExecutor.java | 11 +- .../core/lua/CobaltLuaMachine.java | 20 +-- .../computercraft/core/lua/ILuaMachine.java | 4 +- .../core/lua/MachineEnvironment.java | 49 +++++ .../computercraft/core/metrics/Metric.java | 141 +++++++++++++++ .../computercraft/core/metrics/Metrics.java | 41 +++++ .../core/metrics/MetricsObserver.java | 36 ++++ .../core/tracking/ComputerMBean.java | 156 ---------------- .../core/tracking/ComputerTracker.java | 122 ------------- .../computercraft/core/tracking/Tracker.java | 47 ----- .../computercraft/core/tracking/Tracking.java | 87 --------- .../core/tracking/TrackingContext.java | 116 ------------ .../core/tracking/TrackingField.java | 96 ---------- .../computercraft/shared/CommonHooks.java | 6 +- .../shared/command/CommandComputerCraft.java | 66 ++++--- .../arguments/ArgumentSerializers.java | 2 +- .../arguments/TrackingFieldArgumentType.java | 13 +- .../shared/computer/core/ServerComputer.java | 11 +- .../shared/computer/core/ServerContext.java | 12 ++ .../computer/metrics/ComputerMBean.java | 167 ++++++++++++++++++ .../metrics/ComputerMetricsObserver.java | 35 ++++ .../computer/metrics/GlobalMetrics.java | 121 +++++++++++++ .../computer/metrics/basic/Aggregate.java | 33 ++++ .../metrics/basic/AggregatedMetric.java | 62 +++++++ .../basic/BasicComputerMetricsObserver.java | 89 ++++++++++ .../metrics/basic/ComputerMetrics.java | 129 ++++++++++++++ .../shared/turtle/apis/TurtleAPI.java | 10 +- .../assets/computercraft/lang/en_us.json | 11 +- .../computercraft/core/apis/AsyncRunner.kt | 5 +- .../core/computer/BasicEnvironment.java | 20 ++- .../core/computer/FakeComputerManager.java | 33 +++- 43 files changed, 1107 insertions(+), 760 deletions(-) rename src/main/java/dan200/computercraft/core/{lua => computer}/LuaContext.java (93%) create mode 100644 src/main/java/dan200/computercraft/core/lua/MachineEnvironment.java create mode 100644 src/main/java/dan200/computercraft/core/metrics/Metric.java create mode 100644 src/main/java/dan200/computercraft/core/metrics/Metrics.java create mode 100644 src/main/java/dan200/computercraft/core/metrics/MetricsObserver.java delete mode 100644 src/main/java/dan200/computercraft/core/tracking/ComputerMBean.java delete mode 100644 src/main/java/dan200/computercraft/core/tracking/ComputerTracker.java delete mode 100644 src/main/java/dan200/computercraft/core/tracking/Tracker.java delete mode 100644 src/main/java/dan200/computercraft/core/tracking/Tracking.java delete mode 100644 src/main/java/dan200/computercraft/core/tracking/TrackingContext.java delete mode 100644 src/main/java/dan200/computercraft/core/tracking/TrackingField.java create mode 100644 src/main/java/dan200/computercraft/shared/computer/metrics/ComputerMBean.java create mode 100644 src/main/java/dan200/computercraft/shared/computer/metrics/ComputerMetricsObserver.java create mode 100644 src/main/java/dan200/computercraft/shared/computer/metrics/GlobalMetrics.java create mode 100644 src/main/java/dan200/computercraft/shared/computer/metrics/basic/Aggregate.java create mode 100644 src/main/java/dan200/computercraft/shared/computer/metrics/basic/AggregatedMetric.java create mode 100644 src/main/java/dan200/computercraft/shared/computer/metrics/basic/BasicComputerMetricsObserver.java create mode 100644 src/main/java/dan200/computercraft/shared/computer/metrics/basic/ComputerMetrics.java 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 ) {