Skip to content

Commit

Permalink
Add Lag Spike profiling with /profile s
Browse files Browse the repository at this point in the history
  • Loading branch information
LunNova committed Feb 14, 2016
1 parent 74f736d commit 9c04130
Show file tree
Hide file tree
Showing 3 changed files with 271 additions and 17 deletions.
19 changes: 12 additions & 7 deletions src/main/java/nallar/tickprofiler/minecraft/TickProfiler.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import nallar.tickprofiler.minecraft.commands.ProfileCommand;
import nallar.tickprofiler.minecraft.commands.TPSCommand;
import nallar.tickprofiler.minecraft.profiling.EntityTickProfiler;
import nallar.tickprofiler.minecraft.profiling.LagSpikeProfiler;
import nallar.tickprofiler.reporting.Metrics;
import nallar.tickprofiler.util.TableFormatter;
import nallar.tickprofiler.util.VersionUtil;
Expand All @@ -31,27 +32,30 @@
import net.minecraftforge.common.config.Configuration;
import net.minecraftforge.event.entity.player.PlayerInteractEvent;

import java.io.*;
import java.util.*;
import java.io.File;
import java.util.Arrays;
import java.util.Collections;
import java.util.Set;

@SuppressWarnings("WeakerAccess")
@Mod(modid = "TickProfiler", name = "TickProfiler", acceptableRemoteVersions = "*")
public class TickProfiler {
private static final Set<World> profilingWorlds = Collections.newSetFromMap(new MapMaker().weakKeys().<World, Boolean>makeMap());
@Mod.Instance("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;
private static final Set<World> profilingWorlds = Collections.newSetFromMap(new MapMaker().weakKeys().<World, Boolean>makeMap());

static {
new Metrics("TickProfiler", VersionUtil.versionNumber());
}

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;

static {
new Metrics("TickProfiler", VersionUtil.versionNumber());
}

public static boolean shouldProfile(World w) {
return profilingWorlds.contains(w);
}
Expand Down Expand Up @@ -140,6 +144,7 @@ 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@

import nallar.tickprofiler.Log;
import nallar.tickprofiler.minecraft.TickProfiler;
import nallar.tickprofiler.minecraft.profiling.ContentionProfiler;
import nallar.tickprofiler.minecraft.profiling.EntityTickProfiler;
import nallar.tickprofiler.minecraft.profiling.PacketProfiler;
import nallar.tickprofiler.minecraft.profiling.UtilisationProfiler;
import nallar.tickprofiler.minecraft.profiling.*;
import nallar.tickprofiler.util.TableFormatter;
import net.minecraft.command.ICommandSender;
import net.minecraft.entity.Entity;
Expand Down Expand Up @@ -68,6 +65,8 @@ private boolean process(final ICommandSender commandSender, List<String> argumen
resolution = Integer.valueOf(arguments.get(2));
}
return ContentionProfiler.profile(commandSender, time_, resolution);
case LAG_SPIKE_DETECTOR:
return LagSpikeProfiler.profile(commandSender, time_);
}

if (arguments.size() > 2) {
Expand All @@ -84,10 +83,7 @@ private boolean process(final ICommandSender commandSender, List<String> argumen
z = entity.chunkCoordZ;
}
} catch (UsageException e) {
sendChat(commandSender, "Usage: /profile [e/p/u/l/(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");
sendChat(commandSender, getCommandUsage(commandSender));
return true;
}

Expand Down Expand Up @@ -117,7 +113,10 @@ public void run() {

@Override
public String getCommandUsage(ICommandSender icommandsender) {
return "Usage: /profile [e/(c [chunk x] [chunk z])] [time=30] [dimensionid=all]";
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 {
Expand All @@ -126,7 +125,8 @@ public enum ProfilingState {
CHUNK_ENTITIES("c", 30),
PACKETS("p", 30),
UTILISATION("u", 240),
LOCK_CONTENTION("l", 240);
LOCK_CONTENTION("l", 240),
LAG_SPIKE_DETECTOR("s", 600);

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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
package nallar.tickprofiler.minecraft.profiling;

import com.google.common.base.Function;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import nallar.tickprofiler.Log;
import nallar.tickprofiler.minecraft.commands.Command;
import nallar.tickprofiler.util.CollectionsUtil;
import net.minecraft.command.ICommandSender;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.dedicated.DedicatedServer;

import java.lang.management.*;
import java.util.*;
import java.util.concurrent.TimeUnit;

public class LagSpikeProfiler {
private static final StackTraceElement[] EMPTY_STACK_TRACE = new StackTraceElement[0];
private static final int deadLockMillis = 500;
private static final long deadLockNanoSeconds = TimeUnit.SECONDS.toNanos(deadLockMillis);
private static boolean inProgress;
private static volatile long lastTickTime = 0;
private final ICommandSender commandSender;
private final long stopTime;
private boolean detected;
private boolean stopping;

public LagSpikeProfiler(ICommandSender commandSender, int time_) {
this.commandSender = commandSender;
stopTime = System.nanoTime() + TimeUnit.SECONDS.toNanos(time_);
}

public static boolean profile(ICommandSender commandSender, int time_) {
synchronized (LagSpikeProfiler.class) {
if (inProgress) {
Command.sendChat(commandSender, "Lag spike profiling is already in progress");
return false;
}
inProgress = true;
}
Command.sendChat(commandSender, "Started lag spike detection for " + time_ + " seconds.");
new LagSpikeProfiler(commandSender, time_).start();
return true;
}

public static long tick() {
long nanoTime = System.nanoTime();
tick(nanoTime);
return nanoTime;
}

public static void tick(long nanoTime) {
lastTickTime = nanoTime;
}

private static void printThreadDump(StringBuilder sb) {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
if (deadlockedThreads == null) {
TreeMap<String, String> sortedThreads = sortedThreads(threadMXBean);
sb.append(CollectionsUtil.join(sortedThreads.values(), "\n"));
} else {
ThreadInfo[] infos = threadMXBean.getThreadInfo(deadlockedThreads, true, true);
sb.append("Definitely deadlocked: \n");
for (ThreadInfo threadInfo : infos) {
sb.append(toString(threadInfo, true)).append('\n');
}
}
}

private static TreeMap<String, String> sortedThreads(ThreadMXBean threadMXBean) {
LoadingCache<String, List<ThreadInfo>> threads = CacheBuilder.newBuilder().build(new CacheLoader<String, List<ThreadInfo>>() {
@Override
public List<ThreadInfo> load(final String key) throws Exception {
return new ArrayList<>();
}
});

ThreadInfo[] t = threadMXBean.dumpAllThreads(true, true);
for (ThreadInfo thread : t) {
String info = toString(thread, false);
if (info != null) {
threads.getUnchecked(info).add(thread);
}
}

TreeMap<String, String> sortedThreads = new TreeMap<>();
for (Map.Entry<String, List<ThreadInfo>> entry : threads.asMap().entrySet()) {
List<ThreadInfo> threadInfoList = entry.getValue();
ThreadInfo lowest = null;
for (ThreadInfo threadInfo : threadInfoList) {
if (lowest == null || threadInfo.getThreadName().toLowerCase().compareTo(lowest.getThreadName().toLowerCase()) < 0) {
lowest = threadInfo;
}
}
List threadNameList = CollectionsUtil.newList(threadInfoList, new Function<Object, Object>() {
@Override
public Object apply(final Object input) {
return ((ThreadInfo) input).getThreadName();
}
});
Collections.sort(threadNameList);
sortedThreads.put(lowest.getThreadName(), '"' + CollectionsUtil.join(threadNameList, "\", \"") + "\" " + entry.getKey());
}

return sortedThreads;
}

private static void trySleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException ignored) {
}
}

private static String toString(ThreadInfo threadInfo, boolean name) {
if (threadInfo == null) {
return null;
}
StackTraceElement[] stackTrace = threadInfo.getStackTrace();
if (stackTrace == null) {
stackTrace = EMPTY_STACK_TRACE;
}
StringBuilder sb = new StringBuilder();
if (name) {
sb.append('"').append(threadInfo.getThreadName()).append('"').append(" Id=").append(threadInfo.getThreadId()).append(' ');
}
sb.append(threadInfo.getThreadState());
if (threadInfo.getLockName() != null) {
sb.append(" on ").append(threadInfo.getLockName());
}
if (threadInfo.getLockOwnerName() != null) {
sb.append(" owned by \"").append(threadInfo.getLockOwnerName()).append("\" Id=").append(threadInfo.getLockOwnerId());
}
if (threadInfo.isSuspended()) {
sb.append(" (suspended)");
}
if (threadInfo.isInNative()) {
sb.append(" (in native)");
}
int run = 0;
sb.append('\n');
for (int i = 0; i < stackTrace.length; i++) {
String steString = stackTrace[i].toString();
if (steString.contains(".run(")) {
run++;
}
sb.append("\tat ").append(steString);
sb.append('\n');
if (i == 0 && threadInfo.getLockInfo() != null) {
Thread.State ts = threadInfo.getThreadState();
switch (ts) {
case BLOCKED:
sb.append("\t- blocked on ").append(threadInfo.getLockInfo());
sb.append('\n');
break;
case WAITING:
sb.append("\t- waiting on ").append(threadInfo.getLockInfo());
sb.append('\n');
break;
case TIMED_WAITING:
sb.append("\t- waiting on ").append(threadInfo.getLockInfo());
sb.append('\n');
break;
default:
}
}

for (MonitorInfo mi : threadInfo.getLockedMonitors()) {
if (mi.getLockedStackDepth() == i) {
sb.append("\t- locked ").append(mi);
sb.append('\n');
}
}
}

LockInfo[] locks = threadInfo.getLockedSynchronizers();
if (locks.length > 0) {
sb.append("\n\tNumber of locked synchronizers = ").append(locks.length);
sb.append('\n');
for (LockInfo li : locks) {
sb.append("\t- ").append(li);
sb.append('\n');
}
}
sb.append('\n');
return (run <= 2 && sb.indexOf("at java.util.concurrent.LinkedBlockingQueue.take(") != -1) ? null : sb.toString();
}

private void start() {
final int sleepTime = Math.max(1000, (deadLockMillis * 1000) / 6);
Thread detectorThread = new Thread(new Runnable() {
@Override
public void run() {
trySleep(60000);
while (!stopping && checkForLagSpikes()) {
trySleep(sleepTime);
}
}
});
detectorThread.setName("Lag Spike Detector");
detectorThread.start();
}

public void stop() {
stopping = true;
}

boolean checkForLagSpikes() {
long time = System.nanoTime();

if (time > stopTime)
return false;

long deadTime = time - lastTickTime;
if (deadTime < deadLockNanoSeconds) {
detected = false;
return true;
}

if (detected)
return true;

final MinecraftServer minecraftServer = MinecraftServer.getServer();
if (!minecraftServer.isServerRunning() || minecraftServer.isServerStopped()) {
return false;
}

detected = true;
handleLagSpike(deadTime);
return true;
}

private void handleLagSpike(long deadNanoSeconds) {
StringBuilder sb = new StringBuilder();
sb
.append("The server appears to have ").append("lag spiked.")
.append("\nLast tick ").append(deadNanoSeconds / 1000000000f).append("s ago.");

printThreadDump(sb);

Log.error(sb.toString());

if (commandSender != null && !(commandSender instanceof DedicatedServer))
Command.sendChat(commandSender, "Lag spike detected. See console/log for more information.");
trySleep(15000);
}
}

0 comments on commit 9c04130

Please sign in to comment.