diff --git a/src/main/java/dan200/computercraft/core/apis/IAPIEnvironment.java b/src/main/java/dan200/computercraft/core/apis/IAPIEnvironment.java index 541e34ba8..5c716c4d7 100644 --- a/src/main/java/dan200/computercraft/core/apis/IAPIEnvironment.java +++ b/src/main/java/dan200/computercraft/core/apis/IAPIEnvironment.java @@ -11,6 +11,7 @@ import dan200.computercraft.core.computer.Computer; import dan200.computercraft.core.computer.IComputerEnvironment; import dan200.computercraft.core.filesystem.FileSystem; import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.core.tracking.TrackingField; public interface IAPIEnvironment { @@ -42,4 +43,6 @@ public interface IAPIEnvironment String getLabel(); void setLabel( String label ); + + void addTrackingChange( TrackingField field, long change ); } diff --git a/src/main/java/dan200/computercraft/core/computer/Computer.java b/src/main/java/dan200/computercraft/core/computer/Computer.java index d62ee2be0..f6bfa70e8 100644 --- a/src/main/java/dan200/computercraft/core/computer/Computer.java +++ b/src/main/java/dan200/computercraft/core/computer/Computer.java @@ -20,6 +20,8 @@ import dan200.computercraft.core.filesystem.FileSystemException; import dan200.computercraft.core.lua.CobaltLuaMachine; import dan200.computercraft.core.lua.ILuaMachine; import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.core.tracking.Tracking; +import dan200.computercraft.core.tracking.TrackingField; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -167,6 +169,12 @@ public class Computer m_computer.setLabel( label ); } + @Override + public void addTrackingChange( TrackingField field, long change ) + { + Tracking.addValue( m_computer, field, change ); + } + public void onPeripheralChanged( int side, IPeripheral peripheral ) { synchronized( m_computer.m_peripherals ) diff --git a/src/main/java/dan200/computercraft/core/computer/ComputerThread.java b/src/main/java/dan200/computercraft/core/computer/ComputerThread.java index fa071396e..d97166ce5 100644 --- a/src/main/java/dan200/computercraft/core/computer/ComputerThread.java +++ b/src/main/java/dan200/computercraft/core/computer/ComputerThread.java @@ -7,6 +7,7 @@ package dan200.computercraft.core.computer; import dan200.computercraft.ComputerCraft; +import dan200.computercraft.core.tracking.Tracking; import java.util.HashSet; import java.util.Set; @@ -233,7 +234,7 @@ public class ComputerThread { long stop = System.nanoTime(); Computer computer = task.getOwner(); - if( computer != null ) ComputerTimeTracker.addTiming( computer, stop - start ); + if( computer != null ) Tracking.addTiming( computer, stop - start ); // Re-add it back onto the queue or remove it synchronized( s_taskLock ) diff --git a/src/main/java/dan200/computercraft/core/computer/ComputerTimeTracker.java b/src/main/java/dan200/computercraft/core/computer/ComputerTimeTracker.java deleted file mode 100644 index 0ee9f3aa5..000000000 --- a/src/main/java/dan200/computercraft/core/computer/ComputerTimeTracker.java +++ /dev/null @@ -1,116 +0,0 @@ -package dan200.computercraft.core.computer; - -import com.google.common.collect.MapMaker; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.lang.ref.WeakReference; -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 ComputerTimeTracker -{ - public static class Timings - { - private final WeakReference computer; - private final int computerId; - - private int tasks; - - private long totalTime; - private long maxTime; - - public Timings( @Nonnull Computer computer ) - { - this.computer = new WeakReference<>( computer ); - this.computerId = computer.getID(); - } - - @Nullable - public Computer getComputer() - { - return computer.get(); - } - - public int getComputerId() - { - return computerId; - } - - public int getTasks() - { - return tasks; - } - - public long getTotalTime() - { - return totalTime; - } - - public long getMaxTime() - { - return maxTime; - } - - public double getAverage() - { - return totalTime / (double) tasks; - } - - void update( long time ) - { - tasks++; - totalTime += time; - if( time > maxTime ) maxTime = time; - } - } - - private static boolean tracking; - private static final List timings = new ArrayList<>(); - private static final Map timingLookup = new MapMaker().weakKeys().makeMap(); - - public synchronized static void start() - { - tracking = true; - - timings.clear(); - timingLookup.clear(); - } - - public synchronized static boolean stop() - { - if( !tracking ) return false; - - tracking = false; - timingLookup.clear(); - return true; - } - - public static synchronized List getTimings() - { - return new ArrayList<>( timings ); - } - - public static synchronized void addTiming( Computer computer, long time ) - { - if( !tracking ) return; - - Timings timings = ComputerTimeTracker.timingLookup.get( computer ); - if( timings == null ) - { - timings = new Timings( computer ); - timingLookup.put( computer, timings ); - ComputerTimeTracker.timings.add( timings ); - } - - timings.update( time ); - } -} diff --git a/src/main/java/dan200/computercraft/core/tracking/ComputerTracker.java b/src/main/java/dan200/computercraft/core/tracking/ComputerTracker.java new file mode 100644 index 000000000..64b936a3d --- /dev/null +++ b/src/main/java/dan200/computercraft/core/tracking/ComputerTracker.java @@ -0,0 +1,96 @@ +package dan200.computercraft.core.tracking; + +import dan200.computercraft.core.computer.Computer; +import gnu.trove.map.hash.TObjectLongHashMap; + +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 final TObjectLongHashMap fields; + + public ComputerTracker( Computer computer ) + { + this.computer = new WeakReference<>( computer ); + this.computerId = computer.getID(); + this.fields = new TObjectLongHashMap<>(); + } + + ComputerTracker( ComputerTracker timings ) + { + this.computer = timings.computer; + this.computerId = timings.computerId; + + this.tasks = timings.tasks; + this.totalTime = timings.totalTime; + this.maxTime = timings.maxTime; + this.fields = new TObjectLongHashMap<>( 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 addTiming( long time ) + { + tasks++; + totalTime += time; + if( time > maxTime ) maxTime = time; + } + + void addValue( TrackingField field, long change ) + { + fields.adjustOrPutValue( field, change, 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 totalTime / tasks; + + return fields.get( field ); + } + + public String getFormatted( TrackingField field ) + { + return field.format( get( field ) ); + } +} diff --git a/src/main/java/dan200/computercraft/core/tracking/Tracking.java b/src/main/java/dan200/computercraft/core/tracking/Tracking.java new file mode 100644 index 000000000..219d247fc --- /dev/null +++ b/src/main/java/dan200/computercraft/core/tracking/Tracking.java @@ -0,0 +1,52 @@ +package dan200.computercraft.core.tracking; + +import dan200.computercraft.core.computer.Computer; + +import java.util.HashMap; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +public class Tracking +{ + static final AtomicInteger tracking = new AtomicInteger( 0 ); + private static final HashMap contexts = new HashMap<>(); + + public static TrackingContext getContext( UUID uuid ) + { + synchronized( contexts ) + { + TrackingContext context = contexts.get( uuid ); + if( context == null ) contexts.put( uuid, context = new TrackingContext() ); + return context; + } + } + + public static void addTiming( Computer computer, long time ) + { + if( tracking.get() == 0 ) return; + + synchronized( contexts ) + { + for( TrackingContext context : contexts.values() ) context.addTiming( computer, time ); + } + } + + public static void addValue( Computer computer, TrackingField field, long change ) + { + if( tracking.get() == 0 ) return; + + synchronized( contexts ) + { + for( TrackingContext context : contexts.values() ) context.addValue( computer, field, change ); + } + } + + public static void reset() + { + synchronized( contexts ) + { + contexts.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 new file mode 100644 index 000000000..cc99ed144 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/tracking/TrackingContext.java @@ -0,0 +1,85 @@ +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 +{ + 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 getTimings() + { + ArrayList timings = new ArrayList<>( this.timings.size() ); + for( ComputerTracker timing : this.timings ) timings.add( new ComputerTracker( timing ) ); + return timings; + } + + public void addTiming( 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.addTiming( time ); + } + } + + public synchronized 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 new file mode 100644 index 000000000..392fb42fb --- /dev/null +++ b/src/main/java/dan200/computercraft/core/tracking/TrackingField.java @@ -0,0 +1,54 @@ +package dan200.computercraft.core.tracking; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.LongFunction; + +public class TrackingField +{ + private static final Map fields = new HashMap<>(); + + public static final TrackingField TASKS = TrackingField.of( "tasks", "Tasks", x -> String.format( "%4d", x ) ); + public static final TrackingField TOTAL_TIME = TrackingField.of( "total", "Total time", x -> String.format( "%7.1fms", x / 1e6 ) ); + public static final TrackingField AVERAGE_TIME = TrackingField.of( "average", "Average time", x -> String.format( "%4.1fms", x / 1e6 ) ); + public static final TrackingField MAX_TIME = TrackingField.of( "max", "Max time", x -> String.format( "%5.1fms", x / 1e6 ) ); + + private final String id; + private final String displayName; + private final LongFunction format; + + public String id() + { + return id; + } + + public String displayName() + { + return displayName; + } + + private TrackingField( String id, String displayName, LongFunction format ) + { + this.id = id; + this.displayName = displayName; + this.format = format; + } + + public String format( long value ) + { + return format.apply( value ); + } + + public static TrackingField of( String id, String displayName, LongFunction format ) + { + TrackingField field = new TrackingField( id, displayName, format ); + fields.put( id, field ); + return field; + } + + public static Map fields() + { + return Collections.unmodifiableMap( fields ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java b/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java index 8036185bd..df602b534 100644 --- a/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java +++ b/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java @@ -4,9 +4,13 @@ import com.google.common.collect.Sets; import dan200.computercraft.ComputerCraft; import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.core.computer.Computer; -import dan200.computercraft.core.computer.ComputerTimeTracker; +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.shared.command.framework.*; import dan200.computercraft.shared.computer.core.ServerComputer; +import net.minecraft.command.CommandBase; import net.minecraft.command.CommandException; import net.minecraft.command.ICommandSender; import net.minecraft.entity.Entity; @@ -25,6 +29,8 @@ import static dan200.computercraft.shared.command.framework.ChatHelpers.*; public final class CommandComputerCraft extends CommandDelegate { + public static final UUID SYSTEM_UUID = new UUID( 0, 0 ); + public CommandComputerCraft() { super( create() ); @@ -311,7 +317,7 @@ public final class CommandComputerCraft extends CommandDelegate @Override public void execute( @Nonnull CommandContext context, @Nonnull List arguments ) { - ComputerTimeTracker.start(); + getTimingContext( context ).start(); String stopCommand = "/" + context.parent().getFullPath() + " stop"; context.getSender().sendMessage( list( @@ -330,23 +336,54 @@ public final class CommandComputerCraft extends CommandDelegate @Override public void execute( @Nonnull CommandContext context, @Nonnull List arguments ) throws CommandException { - if( !ComputerTimeTracker.stop() ) throw new CommandException( "Tracking not enabled" ); - displayTimings( context ); + TrackingContext timings = getTimingContext( context ); + if( !timings.stop() ) throw new CommandException( "Tracking not enabled" ); + displayTimings( context, timings.getTimings(), TrackingField.AVERAGE_TIME ); } } ); track.register( new SubCommandBase( - "dump", "Dump the latest track results", UserLevel.OWNER_OP, + "dump", "[kind]", "Dump the latest track results", UserLevel.OWNER_OP, "Dump the latest results of computer tracking." ) { @Override public void execute( @Nonnull CommandContext context, @Nonnull List arguments ) throws CommandException { - displayTimings( context ); + TrackingField field = TrackingField.AVERAGE_TIME; + if( arguments.size() >= 1 ) + { + field = TrackingField.fields().get( arguments.get( 0 ) ); + if( field == null ) throw new CommandException( "Unknown field '" + arguments.get( 0 ) + "'" ); + } + + displayTimings( context, getTimingContext( context ).getTimings(), field ); + } + + @Nonnull + @Override + public List getCompletion( @Nonnull CommandContext context, @Nonnull List arguments ) + { + if( arguments.size() == 1 ) + { + String match = arguments.get( 0 ); + + List out = new ArrayList<>(); + for( String key : TrackingField.fields().keySet() ) + { + if( CommandBase.doesStringStartWith( match, key ) ) out.add( key ); + } + + out.sort( Comparator.naturalOrder() ); + return out; + } + else + { + return super.getCompletion( context, arguments ); + } } } ); - + root.register( new SubCommandBase( "reload", "Reload the ComputerCraft config file", UserLevel.OWNER_OP, "Reload the ComputerCraft config file" @@ -390,13 +427,22 @@ public final class CommandComputerCraft extends CommandDelegate } } - private static void displayTimings( CommandContext context ) throws CommandException + private static TrackingContext getTimingContext( CommandContext context ) { - List timings = ComputerTimeTracker.getTimings(); - if( timings.isEmpty() ) throw new CommandException( "No timings available" ); + Entity entity = context.getSender().getCommandSenderEntity(); + if( entity instanceof EntityPlayerMP ) + { + return Tracking.getContext( entity.getUniqueID() ); + } + else + { + return Tracking.getContext( SYSTEM_UUID ); + } + } - timings.sort( Comparator.comparing( ComputerTimeTracker.Timings::getAverage ).reversed() ); - TextTable table = new TextTable( "Computer", "Tasks", "Total", "Average", "Maximum" ); + private static void displayTimings( CommandContext context, List timings, TrackingField field ) throws CommandException + { + if( timings.isEmpty() ) throw new CommandException( "No timings available" ); Map lookup = new HashMap<>(); int maxId = 0, maxInstance = 0; @@ -411,7 +457,17 @@ public final class CommandComputerCraft extends CommandDelegate ICommandSender sender = context.getSender(); boolean isPlayer = sender instanceof EntityPlayerMP && !(sender instanceof FakePlayer); - for( ComputerTimeTracker.Timings entry : timings ) + timings.sort( Comparator.comparing( x -> x.get( field ) ).reversed() ); + + boolean defaultLayout = field == TrackingField.TASKS || field == TrackingField.TOTAL_TIME + || field == TrackingField.AVERAGE_TIME || field == TrackingField.MAX_TIME; + + + TextTable table = defaultLayout + ? new TextTable( "Computer", "Tasks", "Total", "Average", "Maximum" ) + : new TextTable( "Computer", field.displayName() ); + + for( ComputerTracker entry : timings ) { Computer computer = entry.getComputer(); ServerComputer serverComputer = computer == null ? null : lookup.get( computer ); @@ -437,13 +493,20 @@ public final class CommandComputerCraft extends CommandDelegate ) ); } - table.addRow( - computerComponent, - formatted( "%4d", entry.getTasks() ), - text( String.format( "%7.1f", entry.getTotalTime() / 1e6 ) + "ms" ), - text( String.format( "%4.1f", entry.getAverage() / 1e6 ) + "ms" ), - text( String.format( "%5.1f", entry.getMaxTime() / 1e6 ) + "ms" ) - ); + if( defaultLayout ) + { + table.addRow( + computerComponent, + text( entry.getFormatted( TrackingField.TASKS ) ), + text( entry.getFormatted( TrackingField.TOTAL_TIME ) ), + text( entry.getFormatted( TrackingField.AVERAGE_TIME ) ), + text( entry.getFormatted( TrackingField.MAX_TIME ) ) + ); + } + else + { + table.addRow( computerComponent, text( entry.getFormatted( field ) ) ); + } } table.displayTo( context.getSender() );