Add CPU profiling with WarmRoast "lite."

This commit is contained in:
sk89q 2015-01-13 01:49:59 -08:00
parent 477f1188df
commit 547e89b85d
7 changed files with 555 additions and 15 deletions

View File

@ -20,6 +20,11 @@
package com.sk89q.worldguard.bukkit.commands;
import com.google.common.base.Function;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.sk89q.worldguard.bukkit.WorldGuardPlugin;
import com.sk89q.worldguard.util.paste.EngineHubPaste;
import org.bukkit.ChatColor;
import org.bukkit.command.BlockCommandSender;
import org.bukkit.command.CommandSender;
@ -27,12 +32,17 @@
import org.bukkit.entity.Player;
import javax.annotation.Nullable;
import java.net.URL;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Command-related utility methods.
*/
public final class CommandUtils {
private static final Logger log = Logger.getLogger(CommandUtils.class.getCanonicalName());
private CommandUtils() {
}
@ -153,4 +163,34 @@ public Object apply(@Nullable String s) {
};
}
/**
* Submit data to a pastebin service and inform the sender of
* success or failure.
*
* @param plugin The plugin
* @param sender The sender
* @param content The content
* @param successMessage The message, formatted with {@link String#format(String, Object...)} on success
*/
public static void pastebin(WorldGuardPlugin plugin, final CommandSender sender, String content, final String successMessage) {
ListenableFuture<URL> future = new EngineHubPaste().paste(content);
AsyncCommandHelper.wrap(future, plugin, sender)
.registerWithSupervisor("Submitting content to a pastebin service...")
.sendMessageAfterDelay("(Please wait... sending output to pastebin...)");
Futures.addCallback(future, new FutureCallback<URL>() {
@Override
public void onSuccess(URL url) {
sender.sendMessage(ChatColor.YELLOW + String.format(successMessage, url));
}
@Override
public void onFailure(Throwable throwable) {
log.log(Level.WARNING, "Failed to submit pastebin", throwable);
sender.sendMessage(ChatColor.RED + "Failed to submit to a pastebin. Please see console for the error.");
}
});
}
}

View File

@ -19,15 +19,21 @@
package com.sk89q.worldguard.bukkit.commands;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.io.Files;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.MoreExecutors;
import com.sk89q.minecraft.util.commands.*;
import com.sk89q.worldguard.bukkit.ConfigurationManager;
import com.sk89q.worldguard.bukkit.WorldGuardPlugin;
import com.sk89q.worldguard.bukkit.util.logging.LoggerToChatHandler;
import com.sk89q.worldguard.bukkit.util.report.*;
import com.sk89q.worldguard.util.paste.EngineHubPaste;
import com.sk89q.worldguard.util.profiler.SamplerBuilder;
import com.sk89q.worldguard.util.profiler.SamplerBuilder.Sampler;
import com.sk89q.worldguard.util.profiler.ThreadIdFilter;
import com.sk89q.worldguard.util.profiler.ThreadNameFilter;
import com.sk89q.worldguard.util.report.ReportList;
import com.sk89q.worldguard.util.report.SystemInfoReport;
import com.sk89q.worldguard.util.task.Task;
@ -38,12 +44,14 @@
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.lang.management.ThreadInfo;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -52,6 +60,8 @@ public class WorldGuardCommands {
private static final Logger log = Logger.getLogger(WorldGuardCommands.class.getCanonicalName());
private final WorldGuardPlugin plugin;
@Nullable
private Sampler activeSampler;
public WorldGuardCommands(WorldGuardPlugin plugin) {
this.plugin = plugin;
@ -128,23 +138,111 @@ public void report(CommandContext args, final CommandSender sender) throws Comma
if (args.hasFlag('p')) {
plugin.checkPermission(sender, "worldguard.report.pastebin");
sender.sendMessage(ChatColor.YELLOW + "Now uploading to Pastebin...");
CommandUtils.pastebin(plugin, sender, result, "WorldGuard report: %s.report");
}
}
Futures.addCallback(new EngineHubPaste().paste(result), new FutureCallback<URL>() {
@Override
public void onSuccess(URL url) {
sender.sendMessage(ChatColor.YELLOW + "WorldGuard report: " + url + ".report");
}
@Command(aliases = {"profile"}, usage = "[<minutes>]",
desc = "Profile the CPU usage of the server", min = 0, max = 1,
flags = "t:p")
@CommandPermissions("worldguard.profile")
public void profile(final CommandContext args, final CommandSender sender) throws CommandException {
Predicate<ThreadInfo> threadFilter;
String threadName = args.getFlag('t');
final boolean pastebin;
@Override
public void onFailure(Throwable throwable) {
log.log(Level.WARNING, "Failed to submit pastebin", throwable);
sender.sendMessage(ChatColor.RED + "The WorldGuard report could not be saved to a pastebin service. Please see console for the error.");
}
});
if (args.hasFlag('p')) {
plugin.checkPermission(sender, "worldguard.report.pastebin");
pastebin = true;
} else {
pastebin = false;
}
if (threadName == null) {
threadFilter = new ThreadIdFilter(Thread.currentThread().getId());
} else if (threadName.equals("*")) {
threadFilter = Predicates.alwaysTrue();
} else {
threadFilter = new ThreadNameFilter(threadName);
}
int minutes;
if (args.argsLength() == 0) {
minutes = 5;
} else {
minutes = args.getInteger(0);
if (minutes < 1) {
throw new CommandException("You must run the profile for at least 1 minute.");
} else if (minutes > 10) {
throw new CommandException("You can profile for, at maximum, 10 minutes.");
}
}
Sampler sampler;
synchronized (this) {
if (activeSampler != null) {
throw new CommandException("A profile is currently in progress! Please use /wg stopprofile to stop the current profile.");
}
SamplerBuilder builder = new SamplerBuilder();
builder.setThreadFilter(threadFilter);
builder.setRunTime(minutes, TimeUnit.MINUTES);
sampler = activeSampler = builder.start();
}
AsyncCommandHelper.wrap(sampler.getFuture(), plugin, sender)
.formatUsing(minutes)
.registerWithSupervisor("Running CPU profiler for %d minute(s)...")
.sendMessageAfterDelay("(Please wait... profiling for %d minute(s)...)")
.thenTellErrorsOnly("CPU profiling failed.");
sampler.getFuture().addListener(new Runnable() {
@Override
public void run() {
synchronized (WorldGuardCommands.this) {
activeSampler = null;
}
}
}, MoreExecutors.sameThreadExecutor());
Futures.addCallback(sampler.getFuture(), new FutureCallback<Sampler>() {
@Override
public void onSuccess(Sampler result) {
String output = result.toString();
try {
File dest = new File(plugin.getDataFolder(), "profile.txt");
Files.write(output, dest, Charset.forName("UTF-8"));
sender.sendMessage(ChatColor.YELLOW + "CPU profiling data written to " + dest.getAbsolutePath());
} catch (IOException e) {
sender.sendMessage(ChatColor.RED + "Failed to write CPU profiling data: " + e.getMessage());
}
if (pastebin) {
CommandUtils.pastebin(plugin, sender, output, "Profile result: %s.profile");
}
}
@Override
public void onFailure(Throwable throwable) {
}
});
}
@Command(aliases = {"stopprofile"}, usage = "",desc = "Stop a running profile", min = 0, max = 0)
@CommandPermissions("worldguard.profile")
public void stopProfile(CommandContext args, final CommandSender sender) throws CommandException {
synchronized (this) {
if (activeSampler == null) {
throw new CommandException("No CPU profile is currently running.");
}
activeSampler.cancel();
activeSampler = null;
}
sender.sendMessage("The running CPU profile has been stopped.");
}
@Command(aliases = {"flushstates", "clearstates"},

View File

@ -0,0 +1,147 @@
/*
* WorldGuard, a suite of tools for Minecraft
* Copyright (C) sk89q <http://www.sk89q.com>
* Copyright (C) WorldGuard team and contributors
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by the
* Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.sk89q.worldguard.util.profiler;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.*;
import java.util.concurrent.TimeUnit;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
public class SamplerBuilder {
private static final Timer timer = new Timer("WorldGuard Sampler", true);
private int interval = 100;
private long runTime = TimeUnit.MINUTES.toMillis(5);
private Predicate<ThreadInfo> threadFilter = Predicates.alwaysTrue();
public int getInterval() {
return interval;
}
public void setInterval(int interval) {
checkArgument(interval >= 10, "interval >= 10");
this.interval = interval;
}
public Predicate<ThreadInfo> getThreadFilter() {
return threadFilter;
}
public void setThreadFilter(Predicate<ThreadInfo> threadFilter) {
checkNotNull(threadFilter, "threadFilter");
this.threadFilter = threadFilter;
}
public long getRunTime(TimeUnit timeUnit) {
return timeUnit.convert(runTime, TimeUnit.MILLISECONDS);
}
public void setRunTime(long time, TimeUnit timeUnit) {
checkArgument(time > 0, "time > 0");
this.runTime = timeUnit.toMillis(time);
}
public Sampler start() {
Sampler sampler = new Sampler(interval, threadFilter, System.currentTimeMillis() + runTime);
timer.scheduleAtFixedRate(sampler, 0, interval);
return sampler;
}
public static class Sampler extends TimerTask {
private final int interval;
private final Predicate<ThreadInfo> threadFilter;
private final long endTime;
private final SortedMap<String, StackNode> nodes = new TreeMap<String, StackNode>();
private final ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
private final SettableFuture<Sampler> future = SettableFuture.create();
private Sampler(int interval, Predicate<ThreadInfo> threadFilter, long endTime) {
this.interval = interval;
this.threadFilter = threadFilter;
this.endTime = endTime;
}
public ListenableFuture<Sampler> getFuture() {
return future;
}
private Map<String, StackNode> getData() {
return nodes;
}
private StackNode getNode(String name) {
StackNode node = nodes.get(name);
if (node == null) {
node = new StackNode(name);
nodes.put(name, node);
}
return node;
}
@Override
public synchronized void run() {
try {
if (endTime <= System.currentTimeMillis()) {
future.set(this);
cancel();
return;
}
ThreadInfo[] threadDumps = threadBean.dumpAllThreads(false, false);
for (ThreadInfo threadInfo : threadDumps) {
String threadName = threadInfo.getThreadName();
StackTraceElement[] stack = threadInfo.getStackTrace();
if (threadName != null && stack != null && threadFilter.apply(threadInfo)) {
StackNode node = getNode(threadName);
node.log(stack, interval);
}
}
} catch (Throwable t) {
future.setException(t);
cancel();
}
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
for (Map.Entry<String, StackNode> entry : getData().entrySet()) {
builder.append(entry.getKey());
builder.append(" ");
builder.append(entry.getValue().getTotalTime()).append("ms");
builder.append("\n");
entry.getValue().writeString(builder, 1);
}
return builder.toString();
}
}
}

View File

@ -0,0 +1,121 @@
/*
* WorldGuard, a suite of tools for Minecraft
* Copyright (C) sk89q <http://www.sk89q.com>
* Copyright (C) WorldGuard team and contributors
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by the
* Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.sk89q.worldguard.util.profiler;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class StackNode implements Comparable<StackNode> {
private final String name;
private final Map<String, StackNode> children = Maps.newHashMap();
private long totalTime;
public StackNode(String name) {
this.name = name;
}
public String getName() {
return name;
}
public Collection<StackNode> getChildren() {
List<StackNode> list = Lists.newArrayList(children.values());
Collections.sort(list);
return list;
}
public StackNode getChild(String name) {
StackNode child = children.get(name);
if (child == null) {
child = new StackNode(name);
children.put(name, child);
}
return child;
}
public StackNode getChild(String className, String methodName) {
StackTraceNode node = new StackTraceNode(className, methodName);
StackNode child = children.get(node.getName());
if (child == null) {
child = node;
children.put(node.getName(), node);
}
return child;
}
public long getTotalTime() {
return totalTime;
}
public void log(long time) {
totalTime += time;
}
private void log(StackTraceElement[] elements, int skip, long time) {
log(time);
if (elements.length - skip == 0) {
return;
}
StackTraceElement bottom = elements[elements.length - (skip + 1)];
getChild(bottom.getClassName(), bottom.getMethodName())
.log(elements, skip + 1, time);
}
public void log(StackTraceElement[] elements, long time) {
log(elements, 0, time);
}
@Override
public int compareTo(StackNode o) {
return getName().compareTo(o.getName());
}
void writeString(StringBuilder builder, int indent) {
StringBuilder b = new StringBuilder();
for (int i = 0; i < indent; i++) {
b.append(" ");
}
String padding = b.toString();
for (StackNode child : getChildren()) {
builder.append(padding).append(child.getName());
builder.append(" ");
builder.append(child.getTotalTime()).append("ms");
builder.append("\n");
child.writeString(builder, indent + 1);
}
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
writeString(builder, 0);
return builder.toString();
}
}

View File

@ -0,0 +1,54 @@
/*
* WorldGuard, a suite of tools for Minecraft
* Copyright (C) sk89q <http://www.sk89q.com>
* Copyright (C) WorldGuard team and contributors
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by the
* Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.sk89q.worldguard.util.profiler;
import java.util.List;
public class StackTraceNode extends StackNode {
private final String className;
private final String methodName;
public StackTraceNode(String className, String methodName) {
super(className + "." + methodName + "()");
this.className = className;
this.methodName = methodName;
}
public String getClassName() {
return className;
}
public String getMethodName() {
return methodName;
}
@Override
public int compareTo(StackNode o) {
if (getTotalTime() == o.getTotalTime()) {
return 0;
} else if (getTotalTime()> o.getTotalTime()) {
return -1;
} else {
return 1;
}
}
}

View File

@ -0,0 +1,38 @@
/*
* WorldGuard, a suite of tools for Minecraft
* Copyright (C) sk89q <http://www.sk89q.com>
* Copyright (C) WorldGuard team and contributors
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by the
* Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.sk89q.worldguard.util.profiler;
import com.google.common.base.Predicate;
import java.lang.management.ThreadInfo;
public class ThreadIdFilter implements Predicate<ThreadInfo> {
private final long id;
public ThreadIdFilter(long id) {
this.id = id;
}
@Override
public boolean apply(ThreadInfo threadInfo) {
return threadInfo.getThreadId() == id;
}
}

View File

@ -0,0 +1,42 @@
/*
* WorldGuard, a suite of tools for Minecraft
* Copyright (C) sk89q <http://www.sk89q.com>
* Copyright (C) WorldGuard team and contributors
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by the
* Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.sk89q.worldguard.util.profiler;
import com.google.common.base.Predicate;
import java.lang.management.ThreadInfo;
import static com.google.common.base.Preconditions.checkNotNull;
public class ThreadNameFilter implements Predicate<ThreadInfo> {
private final String name;
public ThreadNameFilter(String name) {
checkNotNull(name, "name");
this.name = name;
}
@Override
public boolean apply(ThreadInfo threadInfo) {
return threadInfo.getThreadName().equalsIgnoreCase(name);
}
}