diff --git a/src/main/java/com/sk89q/worldguard/bukkit/commands/CommandUtils.java b/src/main/java/com/sk89q/worldguard/bukkit/commands/CommandUtils.java index 70cf4972..b8cd7f78 100644 --- a/src/main/java/com/sk89q/worldguard/bukkit/commands/CommandUtils.java +++ b/src/main/java/com/sk89q/worldguard/bukkit/commands/CommandUtils.java @@ -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 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() { + @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."); + } + }); + } + } diff --git a/src/main/java/com/sk89q/worldguard/bukkit/commands/WorldGuardCommands.java b/src/main/java/com/sk89q/worldguard/bukkit/commands/WorldGuardCommands.java index 2b5700a2..e64db526 100644 --- a/src/main/java/com/sk89q/worldguard/bukkit/commands/WorldGuardCommands.java +++ b/src/main/java/com/sk89q/worldguard/bukkit/commands/WorldGuardCommands.java @@ -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() { - @Override - public void onSuccess(URL url) { - sender.sendMessage(ChatColor.YELLOW + "WorldGuard report: " + url + ".report"); - } + @Command(aliases = {"profile"}, usage = "[]", + 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 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() { + @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"}, diff --git a/src/main/java/com/sk89q/worldguard/util/profiler/SamplerBuilder.java b/src/main/java/com/sk89q/worldguard/util/profiler/SamplerBuilder.java new file mode 100644 index 00000000..031cb3ab --- /dev/null +++ b/src/main/java/com/sk89q/worldguard/util/profiler/SamplerBuilder.java @@ -0,0 +1,147 @@ +/* + * WorldGuard, a suite of tools for Minecraft + * Copyright (C) sk89q + * 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 . + */ + +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 threadFilter = Predicates.alwaysTrue(); + + public int getInterval() { + return interval; + } + + public void setInterval(int interval) { + checkArgument(interval >= 10, "interval >= 10"); + this.interval = interval; + } + + public Predicate getThreadFilter() { + return threadFilter; + } + + public void setThreadFilter(Predicate 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 threadFilter; + private final long endTime; + + private final SortedMap nodes = new TreeMap(); + private final ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); + private final SettableFuture future = SettableFuture.create(); + + private Sampler(int interval, Predicate threadFilter, long endTime) { + this.interval = interval; + this.threadFilter = threadFilter; + this.endTime = endTime; + } + + public ListenableFuture getFuture() { + return future; + } + + private Map 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 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(); + } + } + +} diff --git a/src/main/java/com/sk89q/worldguard/util/profiler/StackNode.java b/src/main/java/com/sk89q/worldguard/util/profiler/StackNode.java new file mode 100644 index 00000000..6aa2a43e --- /dev/null +++ b/src/main/java/com/sk89q/worldguard/util/profiler/StackNode.java @@ -0,0 +1,121 @@ +/* + * WorldGuard, a suite of tools for Minecraft + * Copyright (C) sk89q + * 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 . + */ + +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 { + + private final String name; + private final Map children = Maps.newHashMap(); + private long totalTime; + + public StackNode(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public Collection getChildren() { + List 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(); + } + +} diff --git a/src/main/java/com/sk89q/worldguard/util/profiler/StackTraceNode.java b/src/main/java/com/sk89q/worldguard/util/profiler/StackTraceNode.java new file mode 100644 index 00000000..18c5f13a --- /dev/null +++ b/src/main/java/com/sk89q/worldguard/util/profiler/StackTraceNode.java @@ -0,0 +1,54 @@ +/* + * WorldGuard, a suite of tools for Minecraft + * Copyright (C) sk89q + * 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 . + */ + +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; + } + } + +} diff --git a/src/main/java/com/sk89q/worldguard/util/profiler/ThreadIdFilter.java b/src/main/java/com/sk89q/worldguard/util/profiler/ThreadIdFilter.java new file mode 100644 index 00000000..ee719ad9 --- /dev/null +++ b/src/main/java/com/sk89q/worldguard/util/profiler/ThreadIdFilter.java @@ -0,0 +1,38 @@ +/* + * WorldGuard, a suite of tools for Minecraft + * Copyright (C) sk89q + * 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 . + */ + +package com.sk89q.worldguard.util.profiler; + +import com.google.common.base.Predicate; + +import java.lang.management.ThreadInfo; + +public class ThreadIdFilter implements Predicate { + + private final long id; + + public ThreadIdFilter(long id) { + this.id = id; + } + + @Override + public boolean apply(ThreadInfo threadInfo) { + return threadInfo.getThreadId() == id; + } +} diff --git a/src/main/java/com/sk89q/worldguard/util/profiler/ThreadNameFilter.java b/src/main/java/com/sk89q/worldguard/util/profiler/ThreadNameFilter.java new file mode 100644 index 00000000..09a48352 --- /dev/null +++ b/src/main/java/com/sk89q/worldguard/util/profiler/ThreadNameFilter.java @@ -0,0 +1,42 @@ +/* + * WorldGuard, a suite of tools for Minecraft + * Copyright (C) sk89q + * 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 . + */ + +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 { + + 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); + } + +}