Skip to content

Commit

Permalink
Significant rewrite of the way profiling commands are handled to allo…
Browse files Browse the repository at this point in the history
…w for future improvements. Add Entity Counting profiler.
  • Loading branch information
LunNova committed Jun 4, 2017
1 parent a079b51 commit 8e2d95d
Show file tree
Hide file tree
Showing 17 changed files with 629 additions and 544 deletions.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ dependencies {
libLoader "com.eclipsesource.minimal-json:minimal-json:0.9.4"
libLoader "org.minimallycorrect.modpatcher:ModPatcher:$mcVersion-SNAPSHOT"
compileOnly 'org.projectlombok:lombok:1.16.16'
testCompileOnly 'org.projectlombok:lombok:1.16.16'
testCompile 'junit:junit:4.12'
}

tasks.withType(JavaCompile) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,27 @@
package org.minimallycorrect.tickprofiler.minecraft;

import com.google.common.base.Charsets;
import com.google.common.collect.MapMaker;
import com.google.common.io.Files;
import net.minecraft.command.ServerCommandManager;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.init.Items;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.world.World;
import net.minecraftforge.common.DimensionManager;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.config.Configuration;
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.event.FMLInitializationEvent;
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
import net.minecraftforge.fml.common.event.FMLServerStartingEvent;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.common.gameevent.TickEvent;
import org.minimallycorrect.modpatcher.api.UsedByPatch;
import org.minimallycorrect.tickprofiler.Log;
import org.minimallycorrect.tickprofiler.minecraft.commands.Command;
import org.minimallycorrect.tickprofiler.minecraft.commands.DumpCommand;
import org.minimallycorrect.tickprofiler.minecraft.commands.ProfileCommand;
import org.minimallycorrect.tickprofiler.minecraft.commands.TPSCommand;
import org.minimallycorrect.tickprofiler.minecraft.profiling.EntityTickProfiler;
import org.minimallycorrect.tickprofiler.minecraft.profiling.LagSpikeProfiler;
import org.minimallycorrect.tickprofiler.util.TableFormatter;

import java.io.*;
Expand All @@ -40,14 +35,15 @@ public class TickProfiler {
public static TickProfiler instance;
public static long tickTime = 20; // Initialise with non-zero value to avoid divide-by-zero errors calculating TPS
public static long lastTickTime;
public static long tickCount = 0;
public boolean requireOpForProfileCommand = true;
public boolean requireOpForDumpCommand = true;
private int profilingInterval = 0;
private String profilingFileName = "world/computer/<computer id>/profile.txt";
private boolean profilingJson = false;

// Called from patch code
@SuppressWarnings("unused")
@UsedByPatch("entityhook.xml")
public static boolean shouldProfile(World w) {
return profilingWorlds.contains(w);
}
Expand Down Expand Up @@ -134,27 +130,14 @@ public void tick(TickEvent.ServerTickEvent tick) {
long time = System.nanoTime();
long thisTickTime = time - lastTickTime;
lastTickTime = time;
LagSpikeProfiler.tick(time);
tickTime = (tickTime * 19 + thisTickTime) / 20;
final EntityTickProfiler entityTickProfiler = EntityTickProfiler.INSTANCE;
entityTickProfiler.tick();
tickCount++;
int profilingInterval = this.profilingInterval;
if (profilingInterval <= 0 || counter++ % (profilingInterval * 60 * 20) != 0) {
return;
}
entityTickProfiler.startProfiling(() -> {
try {
TableFormatter tf = new TableFormatter(FMLCommonHandler.instance().getMinecraftServerInstance());
tf.tableSeparator = "\n";
if (json) {
entityTickProfiler.writeJSONData(profilingFile);
} else {
Files.write(entityTickProfiler.writeStringData(tf, 6).toString(), profilingFile, Charsets.UTF_8);
}
} catch (Throwable t) {
Log.error("Failed to save periodic profiling data to " + profilingFile, t);
}
}, ProfileCommand.ProfilingState.ENTITIES, 10, Arrays.asList(DimensionManager.getWorlds()));
throw new UnsupportedOperationException("Periodic JSON profiling not currently supported - TODO - fix");
// TODO: Should profile here to json/text
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.text.TextComponentString;
import org.minimallycorrect.tickprofiler.Log;
import org.minimallycorrect.tickprofiler.minecraft.profiling.AlreadyRunningException;

import java.util.*;

Expand Down Expand Up @@ -39,7 +40,16 @@ public boolean checkPermission(MinecraftServer server, ICommandSender commandSen

@Override
public final void execute(MinecraftServer server, ICommandSender commandSender, String[] argumentsArray) {
processCommand(commandSender, new ArrayList<>(Arrays.asList(argumentsArray)));
try {
processCommand(commandSender, new ArrayList<>(Arrays.asList(argumentsArray)));
} catch (UsageException e) {
String message = e.getMessage();
if (message != null && !message.isEmpty())
sendChat(commandSender, "Usage exception: " + message);
sendChat(commandSender, getCommandUsage(commandSender));
} catch (AlreadyRunningException e) {
sendChat(commandSender, e.getMessage());
}
}

protected abstract void processCommand(ICommandSender commandSender, List<String> arguments);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package org.minimallycorrect.tickprofiler.minecraft.commands;

import lombok.val;

import java.util.*;

public class Parameters {
private final ArrayList<String> unmatched = new ArrayList<>();
private final HashMap<String, String> map = new HashMap<>();
private final LinkedHashSet<String> expected = new LinkedHashSet<>();

public Parameters(Iterable<String> args) {
for (val arg : args) {
val equals = arg.indexOf('=');
if (equals == -1)
unmatched.add(arg);
else
map.put(arg.substring(0, equals), arg.substring(equals + 1));
}
}

public void order(List<String> keys) {
int i = 0;
int size = unmatched.size();
for (val key : keys) {
if (i < size)
map.put(key, unmatched.get(i++));
expected.add(key);
}
if (i != 0)
unmatched.subList(0, i).clear();
}

public void orderWithDefault(List<String> keys) {
int i = 0;
int size = unmatched.size();
for (int j = 0; j < keys.size(); j += 2) {
val key = keys.get(j);
map.put(key, i < size ? unmatched.get(i++) : keys.get(j + 1));
expected.add(key);
}
if (i != 0)
unmatched.subList(0, i).clear();
}

public void checkUsage(boolean allowExtra) {
for (String s : expected)
if (!map.containsKey(s))
throw new UsageException("Missing required parameter " + s);

if (!allowExtra) {
if (!unmatched.isEmpty())
throw new UsageException("Unmatched parameters " + unmatched);

for (String s : map.keySet())
if (!expected.contains(s))
throw new UsageException("Unexpected parameter " + s);
}
}

public String getString(String type) {
val result = map.get(type);
if (result == null)
throw new UsageException("Missing parameter " + type);
return result;
}

public int getInt(String time) {
return Integer.parseInt(getString(time));
}

void writeExpectedParameters(StringBuilder sb) {
for (val s : expected) {
sb.append(' ');
sb.append(s);
val current = map.get(s);
if (current != null)
sb.append('=').append(current);
}
}

public boolean has(String key) {
return map.containsKey(key);
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
package org.minimallycorrect.tickprofiler.minecraft.commands;

import lombok.val;
import net.minecraft.command.ICommandSender;
import net.minecraft.entity.Entity;
import net.minecraft.world.World;
import net.minecraftforge.common.DimensionManager;
import org.minimallycorrect.tickprofiler.Log;
import org.minimallycorrect.tickprofiler.minecraft.TickProfiler;
import org.minimallycorrect.tickprofiler.minecraft.profiling.*;
import org.minimallycorrect.tickprofiler.util.TableFormatter;
import org.minimallycorrect.tickprofiler.minecraft.profiling.Profile;

import java.util.*;

Expand All @@ -29,122 +25,61 @@ public void processCommand(final ICommandSender commandSender, List<String> argu
process(commandSender, arguments);
}

private void process(final ICommandSender commandSender, List<String> arguments) {
World world = null;
int time_;
Integer x = null;
Integer z = null;
ProfilingState type;
try {
if (arguments.isEmpty())
throw new UsageException();

type = ProfilingState.get(arguments.get(0));
if (type == null)
throw new UsageException();

if (type == ProfilingState.CHUNK_ENTITIES && arguments.size() > 2) {
x = Integer.valueOf(arguments.remove(1));
z = Integer.valueOf(arguments.remove(1));
}

time_ = type.time;

if (arguments.size() > 1) {
time_ = Integer.valueOf(arguments.get(1));
}
private void addPreParameters(Parameters p) {
p.order(Collections.singletonList("type"));
}

switch (type) {
case PACKETS:
PacketProfiler.profile(commandSender, time_);
return;
case UTILISATION:
UtilisationProfiler.profile(commandSender, time_);
return;
case LOCK_CONTENTION:
int resolution = 240;
if (arguments.size() > 2) {
resolution = Integer.valueOf(arguments.get(2));
}
ContentionProfiler.profile(commandSender, time_, resolution);
return;
case LAG_SPIKE_DETECTOR:
LagSpikeProfiler.profile(commandSender, time_);
return;
}
private void addPostParameters(Parameters p) {
p.orderWithDefault(Arrays.asList("output", "commandsender"));
}

if (arguments.size() > 2) {
world = DimensionManager.getWorld(Integer.valueOf(arguments.get(2)));
} else if (type == ProfilingState.CHUNK_ENTITIES && commandSender instanceof Entity) {
world = ((Entity) commandSender).worldObj;
}
if (type == ProfilingState.CHUNK_ENTITIES && x == null) {
if (!(commandSender instanceof Entity)) {
throw new UsageException("/profile c needs chunk arguments when used from console");
}
Entity entity = (Entity) commandSender;
x = entity.chunkCoordX;
z = entity.chunkCoordZ;
private void process(final ICommandSender commandSender, List<String> arguments) {
val p = new Parameters(arguments);
addPreParameters(p);
p.checkUsage(true);
val typeName = p.getString("type");
val type = Profile.Types.byName(typeName);

if (type == null)
throw new UsageException("Unknown profiling type " + typeName);

type.addParameters(p);
addPostParameters(p);
p.checkUsage(false);
String output = p.getString("output");
val targets = new ArrayList<Profile.ProfileTarget>();
for (String s : output.split(",")) {
switch (s.toLowerCase()) {
case "commandsender":
targets.add(Profile.ProfileTarget.commandSender(commandSender));
break;
case "console":
targets.add(Profile.ProfileTarget.console());
break;
default:
throw new UsageException("Unknown output: " + output);
}
} catch (UsageException e) {
sendChat(commandSender, getCommandUsage(commandSender));
return;
}

final List<World> worlds = new ArrayList<>();
if (world == null) {
Collections.addAll(worlds, DimensionManager.getWorlds());
} else {
worlds.add(world);
}
final int time = time_;
final EntityTickProfiler entityTickProfiler = EntityTickProfiler.INSTANCE;
if (!entityTickProfiler.startProfiling(() -> sendChat(commandSender, entityTickProfiler.writeStringData(new TableFormatter(commandSender)).toString()), type, time, worlds)) {
sendChat(commandSender, "Someone else is currently profiling.");
return;
}
if (type == ProfilingState.CHUNK_ENTITIES) {
entityTickProfiler.setLocation(x, z);
val profile = type.create();
try {
profile.start(targets, p);
} finally {
profile.closeIfNeeded();
}
sendChat(commandSender, "Profiling for " + time + " seconds in " + (world == null ? "all worlds " : Log.name(world))
+ (type == ProfilingState.CHUNK_ENTITIES ? " at " + x + ',' + z : ""));
}

@Override
public String getCommandUsage(ICommandSender icommandsender) {
return "Usage: /profile [e/p/u/l/s/(c [chunkX] [chunk z])] timeInSeconds dimensionID\n" +
"example - profile for 30 seconds in chunk 8,1 in all worlds: /profile c 8 1\n" +
"example - profile for 10 seconds in dimension 4: /profile e 10 4\n" +
"example - profile packets: /profile p";
}

public enum ProfilingState {
NONE(null, 0),
ENTITIES("e", 30),
CHUNK_ENTITIES("c", 30),
PACKETS("p", 30),
UTILISATION("u", 240),
LOCK_CONTENTION("l", 240),
LAG_SPIKE_DETECTOR("s", 600);

static final Map<String, ProfilingState> states = new HashMap<>();

static {
for (ProfilingState p : ProfilingState.values()) {
states.put(p.shortcut, p);
}
}

final String shortcut;
final int time;

ProfilingState(String shortcut, int time) {
this.shortcut = shortcut;
this.time = time;
}

public static ProfilingState get(String shortcut) {
return states.get(shortcut.toLowerCase());
val sb = new StringBuilder("Usage:\n");
for (val type : Profile.Types.values()) {
val p = new Parameters(Collections.singletonList(type.shortName));
type.addParameters(p);
addPostParameters(p);
sb.append(type.name()).append(": ").append("/profile ").append(type.shortName);
p.writeExpectedParameters(sb);
sb.append('\n');
}
return sb.toString();
}
}

0 comments on commit 8e2d95d

Please sign in to comment.