diff options
author | Albert Pham <the.sk89q@gmail.com> | 2013-06-13 15:55:12 -0700 |
---|---|---|
committer | Albert Pham <the.sk89q@gmail.com> | 2013-06-13 15:55:12 -0700 |
commit | 280ad8b8bf2942f055daf5fb6f8ae55a7f88243a (patch) | |
tree | 05b6fa56f6b826242f6a42f1f8fb2514e3d568d9 | |
download | spark-280ad8b8bf2942f055daf5fb6f8ae55a7f88243a.tar.gz spark-280ad8b8bf2942f055daf5fb6f8ae55a7f88243a.tar.bz2 spark-280ad8b8bf2942f055daf5fb6f8ae55a7f88243a.zip |
Initial commit.
-rw-r--r-- | .gitignore | 32 | ||||
-rw-r--r-- | README.md | 77 | ||||
-rw-r--r-- | pom.xml | 145 | ||||
-rw-r--r-- | src/main/java/com/sk89q/warmroast/ClassMapping.java | 67 | ||||
-rw-r--r-- | src/main/java/com/sk89q/warmroast/DataViewServlet.java | 76 | ||||
-rw-r--r-- | src/main/java/com/sk89q/warmroast/McpMapping.java | 112 | ||||
-rw-r--r-- | src/main/java/com/sk89q/warmroast/RoastOptions.java | 49 | ||||
-rw-r--r-- | src/main/java/com/sk89q/warmroast/StackNode.java | 162 | ||||
-rw-r--r-- | src/main/java/com/sk89q/warmroast/StackTraceNode.java | 96 | ||||
-rw-r--r-- | src/main/java/com/sk89q/warmroast/WarmRoast.java | 309 | ||||
-rw-r--r-- | src/main/resources/www/index.html | 13 | ||||
-rw-r--r-- | src/main/resources/www/style.css | 166 | ||||
-rw-r--r-- | src/main/resources/www/warmroast.js | 43 |
13 files changed, 1347 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..82db09d --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Eclipse stuff +/.classpath +/.project +/.settings + +# netbeans +/nbproject + +# we use maven! +/build.xml + +# maven +/target + +# vim +.*.sw[a-p] + +# various other potential build files +/build +/bin +/dist +/manifest.mf +/dependency-reduced-pom.xml + +# Mac filesystem dust +/.DS_Store + +# intellij +*.iml +*.ipr +*.iws +.idea/
\ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f8edd13 --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +WarmRoast +========= + +WarmRoast is an easy-to-use CPU sampling tool for JVM applications, but particularly suited for Minecraft servers/clients. + +* Adjustable sampling frequency. +* Supports loading MCP mappings for deobfuscating class and method names. +* Web-based — perform the profiling on a remote server and view the results in your browser. + * Collapse and expand nodes to see details. + * Easily view CPU usage per method at a glance. + * Hover to highlight all child methods as a group. + * See the percentage of CPU time for each method relative to its parent methods. + * Maintains style and function with use of "File -> Save As" (in tested browsers). + +**Download Latest Version:** http://builds.enginehub.org/job/warmroast/last-successful/ + +Java 7 and above is required to use WarmRoast. + +Screenshots +----------- + +![Sample output](http://i.imgur.com/KCDYkIv.png) + +Usage +----- + +1. Note the path of your JDK. + +2. Download WarmRoast as `warmroast.jar`. + +3. Replace `/path/to/jdk` in the following command line with the path to your JDK and execute the program. + +### Linux ### + + java -Djava.library.path=/path/to/jdk/jre/bin -cp /path/to/jdk/lib/tools.jar:warmroast.jar com.sk89q.warmroast.WarmRoast + +### Windows ### + + java -Djava.library.path=/path/to/jdk/jre/bin -cp /path/to/jdk/lib/tools.jar;warmroast.jar com.sk89q.warmroast.WarmRoast + +Parameters +---------- + + warmroast.WarmRoast --help + Usage: warmroast [options] + Options: + --bind + The address to bind the HTTP server to + Default: 0.0.0.0 + -h, --help + + Default: false + --interval + The sample rate, in milliseconds + Default: 100 + -m, --mappings + A directory with joined.srg and methods.csv + --name + The name of the VM to attach to + --pid + The PID of the VM to attach to + -p, --port + The port to bind the HTTP server to + Default: 23000 + -t, --thread + Optionally specify a thread to log only + +Hint: `--thread "Server thread"` is useful for Minecraft servers. + +License +------- + +The launcher is licensed under the GNU General Public License, version 3. + +Contributions by third parties must be dual licensed under the two licenses +described within LICENSE.txt (GNU General Public License, version 3, and the +3-clause BSD license). @@ -0,0 +1,145 @@ +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <groupId>com.sk89q</groupId> + <artifactId>warmroast</artifactId> + <version>1.0.0-SNAPSHOT</version> + <name>WarmRoast</name> + <url>http://www.sk89q.com</url> + <scm> + <connection>scm:git:git://github.com/sk89q/warmroast.git</connection> + <url>https://github.com/sk89q/warmroast</url> + <developerConnection>scm:git:git@github.com:sk89q/warmroast.git</developerConnection> + </scm> + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + </properties> + <dependencies> + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-servlet</artifactId> + <version>9.0.3.v20130506</version> + </dependency> + <dependency> + <groupId>commons-io</groupId> + <artifactId>commons-io</artifactId> + <version>2.4</version> + </dependency> + <dependency> + <groupId>net.sf.opencsv</groupId> + <artifactId>opencsv</artifactId> + <version>2.0</version> + </dependency> + <dependency> + <groupId>com.beust</groupId> + <artifactId>jcommander</artifactId> + <version>1.30</version> + </dependency> + </dependencies> + <build> + <resources> + <resource> + <targetPath>www</targetPath> + <filtering>false</filtering> + <directory>${basedir}/src/main/resources/www</directory> + <includes> + <include>**/*</include> + </includes> + </resource> + </resources> + <plugins> + <plugin> + <artifactId>maven-compiler-plugin</artifactId> + <version>3.0</version> + <configuration> + <source>1.7</source> + <target>1.7</target> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jar-plugin</artifactId> + <configuration> + <archive> + <manifest> + <mainClass>com.sk89q.warmroast.WarmRoast</mainClass> + </manifest> + </archive> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-shade-plugin</artifactId> + <version>2.1</version> + <executions> + <execution> + <phase>package</phase> + <goals> + <goal>shade</goal> + </goals> + <configuration> + <filters> + <filter> + <artifact>*:*</artifact> + <excludes> + <exclude>META-INF/*.SF</exclude> + <exclude>META-INF/*.DSA</exclude> + <exclude>META-INF/*.RSA</exclude> + </excludes> + </filter> + </filters> + <artifactSet> + <excludes> + <exclude>com.sun:tools</exclude> + </excludes> + </artifactSet> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + <profiles> + <profile> + <id>tools-default</id> + <activation> + <activeByDefault>true</activeByDefault> + <file> + <exists>${java.home}/../lib/tools.jar</exists> + </file> + </activation> + <properties> + <toolsJar>${java.home}/../lib/tools.jar</toolsJar> + </properties> + <dependencies> + <dependency> + <groupId>com.sun</groupId> + <artifactId>tools</artifactId> + <version>1.6.0</version> + <scope>system</scope> + <systemPath>${toolsJar}</systemPath> + </dependency> + </dependencies> + </profile> + <profile> + <id>tools-mac</id> + <activation> + <activeByDefault>false</activeByDefault> + <file> + <exists>${java.home}/../Classes/classes.jar</exists> + </file> + </activation> + <properties> + <toolsJar>${java.home}/../Classes/classes.jar</toolsJar> + </properties> + <dependencies> + <dependency> + <groupId>com.sun</groupId> + <artifactId>tools</artifactId> + <version>1.6.0</version> + <scope>system</scope> + <systemPath>${toolsJar}</systemPath> + </dependency> + </dependencies> + </profile> + </profiles> +</project>
\ No newline at end of file diff --git a/src/main/java/com/sk89q/warmroast/ClassMapping.java b/src/main/java/com/sk89q/warmroast/ClassMapping.java new file mode 100644 index 0000000..ad3f7c0 --- /dev/null +++ b/src/main/java/com/sk89q/warmroast/ClassMapping.java @@ -0,0 +1,67 @@ +/* + * WarmRoast + * Copyright (C) 2013 Albert Pham <http://www.sk89q.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package com.sk89q.warmroast; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ClassMapping { + + private final String obfuscated; + private final String actual; + private final Map<String, List<String>> methods = new HashMap<>(); + + public ClassMapping(String obfuscated, String actual) { + this.obfuscated = obfuscated; + this.actual = actual; + } + + public String getObfuscated() { + return obfuscated; + } + + public String getActual() { + return actual; + } + + public void addMethod(String obfuscated, String actual) { + List<String> m = methods.get(obfuscated); + if (m == null) { + m = new ArrayList<>(); + methods.put(obfuscated, m); + } + m.add(actual); + } + + public List<String> mapMethod(String obfuscated) { + List<String> m = methods.get(obfuscated); + if (m == null) { + return new ArrayList<>(); + } + return m; + } + + @Override + public String toString() { + return getObfuscated() + "->" + getActual(); + } + +} diff --git a/src/main/java/com/sk89q/warmroast/DataViewServlet.java b/src/main/java/com/sk89q/warmroast/DataViewServlet.java new file mode 100644 index 0000000..205dd3e --- /dev/null +++ b/src/main/java/com/sk89q/warmroast/DataViewServlet.java @@ -0,0 +1,76 @@ +/* + * WarmRoast + * Copyright (C) 2013 Albert Pham <http://www.sk89q.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package com.sk89q.warmroast; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Collection; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class DataViewServlet extends HttpServlet { + + private static final long serialVersionUID = -2331397310804298286L; + + private final WarmRoast roast; + + public DataViewServlet(WarmRoast roast) { + this.roast = roast; + } + + @Override + protected void doGet(HttpServletRequest request, + HttpServletResponse response) throws ServletException, IOException { + response.setContentType("text/html; charset=utf-8"); + response.setStatus(HttpServletResponse.SC_OK); + + PrintWriter w = response.getWriter(); + w.println("<!DOCTYPE html><html><head><title>WarmRoast</title>"); + w.println("<link rel=\"stylesheet\" type=\"text/css\" href=\"style.css\">"); + w.println("</head><body>"); + w.println("<h1>WarmRoast</h1>"); + w.println("<div class=\"loading\">Downloading snapshot; please wait...</div>"); + w.println("<div class=\"stack\" style=\"display: none\">"); + synchronized (roast) { + Collection<StackNode> nodes = roast.getData().values(); + for (StackNode node : nodes) { + w.println(node.toHtml(roast.getMapping())); + } + if (nodes.size() == 0) { + w.println("<p class=\"no-results\">There are no results. " + + "(Thread filter does not match thread?)</p>"); + } + } + w.println("</div>"); + w.println("<p class=\"legend\">Legend: "); + w.println("<span class=\"matched\">Mapped</span> "); + w.println("<span class=\"multiple-matches\">Multiple Mappings</span> "); + w.println("</p>"); + w.println("<div id=\"overlay\"></div>"); + w.println("<p class=\"footer\">"); + w.println("Icons from <a href=\"http://www.fatcow.com/\">FatCow</a> — "); + w.println("<a href=\"http://github.com/sk89q/warmroast\">github.com/sk89q/warmroast</a></p>"); + w.println("<script src=\"//ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js\"></script>"); + w.println("<script src=\"warmroast.js\"></script>"); + w.println("</body></html>"); + } +}
\ No newline at end of file diff --git a/src/main/java/com/sk89q/warmroast/McpMapping.java b/src/main/java/com/sk89q/warmroast/McpMapping.java new file mode 100644 index 0000000..5ef2ff9 --- /dev/null +++ b/src/main/java/com/sk89q/warmroast/McpMapping.java @@ -0,0 +1,112 @@ +/* + * WarmRoast + * Copyright (C) 2013 Albert Pham <http://www.sk89q.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package com.sk89q.warmroast; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.io.FileUtils; + +import au.com.bytecode.opencsv.CSVReader; + +public class McpMapping { + + private static final Pattern clPattern = + Pattern.compile("CL: (?<obfuscated>[^ ]+) (?<actual>[^ ]+)"); + private static final Pattern mdPattern = + Pattern.compile("MD: (?<obfuscatedClass>[^ /]+)/(?<obfuscatedMethod>[^ ]+) " + + "[^ ]+ (?<method>[^ ]+) [^ ]+"); + + private final Map<String, ClassMapping> classes = new HashMap<>(); + private final Map<String, String> methods = new HashMap<>(); + + public ClassMapping mapClass(String obfuscated) { + return classes.get(obfuscated); + } + + public void read(File joinedFile, File methodsFile) throws IOException { + try (FileReader r = new FileReader(methodsFile)) { + try (CSVReader reader = new CSVReader(r)) { + List<String[]> entries = reader.readAll(); + processMethodNames(entries); + } + } + + List<String> lines = FileUtils.readLines(joinedFile, "UTF-8"); + processClasses(lines); + processMethods(lines); + } + + public String fromMethodId(String id) { + String method = methods.get(id); + if (method == null) { + return id; + } + return method; + } + + private void processMethodNames(List<String[]> entries) { + boolean first = true; + for (String[] entry : entries) { + if (entry.length < 2) { + continue; + } + if (first) { // Header + first = false; + continue; + } + methods.put(entry[0], entry[1]); + } + } + + private void processClasses(List<String> lines) { + for (String line : lines) { + Matcher m = clPattern.matcher(line); + if (m.matches()) { + String obfuscated = m.group("obfuscated"); + String actual = m.group("actual").replace("/", "."); + classes.put(obfuscated, new ClassMapping(obfuscated, actual)); + } + } + } + + private void processMethods(List<String> lines) { + for (String line : lines) { + Matcher m = mdPattern.matcher(line); + if (m.matches()) { + String obfuscatedClass = m.group("obfuscatedClass"); + String obfuscatedMethod = m.group("obfuscatedMethod"); + String method = m.group("method"); + String methodId = method.substring(method.lastIndexOf('/') + 1); + ClassMapping mapping = mapClass(obfuscatedClass); + if (mapping != null) { + mapping.addMethod(obfuscatedMethod, + fromMethodId(methodId)); + } + } + } + } + +} diff --git a/src/main/java/com/sk89q/warmroast/RoastOptions.java b/src/main/java/com/sk89q/warmroast/RoastOptions.java new file mode 100644 index 0000000..edb6a0e --- /dev/null +++ b/src/main/java/com/sk89q/warmroast/RoastOptions.java @@ -0,0 +1,49 @@ +/* + * WarmRoast + * Copyright (C) 2013 Albert Pham <http://www.sk89q.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package com.sk89q.warmroast; + +import com.beust.jcommander.Parameter; + +public class RoastOptions { + + @Parameter(names = { "-h", "--help" }, help = true) + public boolean help; + + @Parameter(names = { "--bind" }, description = "The address to bind the HTTP server to") + public String bindAddress = "0.0.0.0"; + + @Parameter(names = { "-p", "--port" }, description = "The port to bind the HTTP server to") + public Integer port = 23000; + + @Parameter(names = { "--pid" }, description = "The PID of the VM to attach to") + public Integer pid; + + @Parameter(names = { "--name" }, description = "The name of the VM to attach to") + public String vmName; + + @Parameter(names = { "-t", "--thread" }, description = "Optionally specify a thread to log only") + public String threadName; + + @Parameter(names = { "-m", "--mappings" }, description = "A directory with joined.srg and methods.csv") + public String mappingsDir; + + @Parameter(names = { "--interval" }, description = "The sample rate, in milliseconds") + public Integer interval = 100; + +} diff --git a/src/main/java/com/sk89q/warmroast/StackNode.java b/src/main/java/com/sk89q/warmroast/StackNode.java new file mode 100644 index 0000000..216ca5f --- /dev/null +++ b/src/main/java/com/sk89q/warmroast/StackNode.java @@ -0,0 +1,162 @@ +/* + * WarmRoast + * Copyright (C) 2013 Albert Pham <http://www.sk89q.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package com.sk89q.warmroast; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class StackNode implements Comparable<StackNode> { + + private final String name; + private final Map<String, StackNode> children = new HashMap<>(); + private long totalTime; + + public StackNode(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public String getNameHtml(McpMapping mapping) { + return escapeHtml(getName()); + } + + public Collection<StackNode> getChildren() { + List<StackNode> list = new ArrayList<>(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()); + } + + private void writeHtml(StringBuilder builder, McpMapping mapping, long totalTime) { + builder.append("<div class=\"node collapsed\">"); + builder.append("<div class=\"name\">"); + builder.append(getNameHtml(mapping)); + builder.append("<span class=\"percent\">"); + builder + .append(String.format("%.2f", getTotalTime() / (double) totalTime * 100)) + .append("%"); + builder.append("</span>"); + builder.append("<span class=\"time\">"); + builder.append(getTotalTime()).append("ms"); + builder.append("</span>"); + builder.append("<span class=\"bar\">"); + builder.append("<span class=\"bar-inner\" style=\"width:") + .append(String.format("%.2f", getTotalTime() / (double) totalTime * 100)) + .append("%\">"); + builder.append("</span>"); + builder.append("</span>"); + builder.append("</div>"); + builder.append("<ul class=\"children\">"); + for (StackNode child : getChildren()) { + builder.append("<li>"); + child.writeHtml(builder, mapping, totalTime); + builder.append("</li>"); + } + builder.append("</ul>"); + builder.append("</div>"); + } + + public String toHtml(McpMapping mapping) { + StringBuilder builder = new StringBuilder(); + writeHtml(builder, mapping, getTotalTime()); + return builder.toString(); + } + + private 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(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(); + } + + protected static String escapeHtml(String str) { + return str.replace("&", "&").replace("<", "<").replace(">", ">"); + } + +} diff --git a/src/main/java/com/sk89q/warmroast/StackTraceNode.java b/src/main/java/com/sk89q/warmroast/StackTraceNode.java new file mode 100644 index 0000000..1857cd1 --- /dev/null +++ b/src/main/java/com/sk89q/warmroast/StackTraceNode.java @@ -0,0 +1,96 @@ +/* + * WarmRoast + * Copyright (C) 2013 Albert Pham <http://www.sk89q.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package com.sk89q.warmroast; + +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 String getNameHtml(McpMapping mapping) { + ClassMapping classMapping = mapping.mapClass(getClassName()); + if (classMapping != null) { + String className = "<span class=\"matched\" title=\"" + + escapeHtml(getClassName()) + "\">" + + escapeHtml(classMapping.getActual()) + "</span>"; + + List<String> actualMethods = classMapping.mapMethod(getMethodName()); + if (actualMethods.size() == 0) { + return className + "." + escapeHtml(getMethodName()) + "()"; + } else if (actualMethods.size() == 1) { + return className + + ".<span class=\"matched\" title=\"" + + escapeHtml(getMethodName()) + "\">" + + escapeHtml(actualMethods.get(0)) + "</span>()"; + } else { + StringBuilder builder = new StringBuilder(); + boolean first = true; + for (String m : actualMethods) { + if (!first) { + builder.append(" "); + } + builder.append(m); + first = false; + } + return className + + ".<span class=\"multiple-matches\" title=\"" + + builder.toString() + "\">" + escapeHtml(getMethodName()) + "</span>()"; + } + } else { + String actualMethod = mapping.fromMethodId(getMethodName()); + if (actualMethod == null) { + return escapeHtml(getClassName()) + "." + escapeHtml(getMethodName()) + "()"; + } else { + return className + + ".<span class=\"matched\" title=\"" + + escapeHtml(getMethodName()) + "\">" + + escapeHtml(actualMethod) + "</span>()"; + } + } + } + + @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/warmroast/WarmRoast.java b/src/main/java/com/sk89q/warmroast/WarmRoast.java new file mode 100644 index 0000000..6ddd9ca --- /dev/null +++ b/src/main/java/com/sk89q/warmroast/WarmRoast.java @@ -0,0 +1,309 @@ +/* + * WarmRoast + * Copyright (C) 2013 Albert Pham <http://www.sk89q.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package com.sk89q.warmroast; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadInfo; +import java.lang.management.ThreadMXBean; +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.Timer; +import java.util.TimerTask; +import java.util.TreeMap; + +import javax.management.MBeanServerConnection; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXConnectorFactory; +import javax.management.remote.JMXServiceURL; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.HandlerList; +import org.eclipse.jetty.server.handler.ResourceHandler; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; + +import com.beust.jcommander.JCommander; +import com.sun.tools.attach.AgentInitializationException; +import com.sun.tools.attach.AgentLoadException; +import com.sun.tools.attach.AttachNotSupportedException; +import com.sun.tools.attach.VirtualMachine; +import com.sun.tools.attach.VirtualMachineDescriptor; + +public class WarmRoast extends TimerTask { + + private static final String SEPARATOR = + "------------------------------------------------------------------------"; + + private final int interval; + private final VirtualMachine vm; + private final Timer timer = new Timer("Roast Pan", true); + private final McpMapping mapping = new McpMapping(); + private final SortedMap<String, StackNode> nodes = new TreeMap<>(); + private JMXConnector connector; + private MBeanServerConnection mbsc; + private ThreadMXBean threadBean; + private String filterThread; + + public WarmRoast(VirtualMachine vm, int interval) { + this.vm = vm; + this.interval = interval; + } + + public 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; + } + + public McpMapping getMapping() { + return mapping; + } + + public String getFilterThread() { + return filterThread; + } + + public void setFilterThread(String filterThread) { + this.filterThread = filterThread; + } + + public void connect() + throws IOException, AgentLoadException, AgentInitializationException { + // Load the agent + String connectorAddr = vm.getAgentProperties().getProperty( + "com.sun.management.jmxremote.localConnectorAddress"); + if (connectorAddr == null) { + String agent = vm.getSystemProperties().getProperty("java.home") + + File.separator + "lib" + File.separator + + "management-agent.jar"; + vm.loadAgent(agent); + connectorAddr = vm.getAgentProperties().getProperty( + "com.sun.management.jmxremote.localConnectorAddress"); + } + + // Connect + JMXServiceURL serviceURL = new JMXServiceURL(connectorAddr); + connector = JMXConnectorFactory.connect(serviceURL); + mbsc = connector.getMBeanServerConnection(); + try { + threadBean = getThreadMXBean(); + } catch (MalformedObjectNameException e) { + throw new IOException("Bad MX bean name", e); + } + } + + private ThreadMXBean getThreadMXBean() + throws IOException, MalformedObjectNameException { + ObjectName objName = new ObjectName(ManagementFactory.THREAD_MXBEAN_NAME); + Set<ObjectName> mbeans = mbsc.queryNames(objName, null); + for (ObjectName name : mbeans) { + return ManagementFactory.newPlatformMXBeanProxy( + mbsc, name.toString(), ThreadMXBean.class); + } + throw new IOException("No thread MX bean found"); + } + + @Override + public synchronized void run() { + ThreadInfo[] threadDumps = threadBean.dumpAllThreads(false, false); + for (ThreadInfo threadInfo : threadDumps) { + String threadName = threadInfo.getThreadName(); + StackTraceElement[] stack = threadInfo.getStackTrace(); + + if (threadName == null || stack == null) { + continue; + } + + if (filterThread != null && !filterThread.equals(threadName)) { + continue; + } + + StackNode node = getNode(threadName); + node.log(stack, interval); + } + } + + public void start(InetSocketAddress address) throws Exception { + timer.scheduleAtFixedRate(this, interval, interval); + + Server server = new Server(address); + + ServletContextHandler context = new ServletContextHandler(); + context.setContextPath("/"); + context.addServlet(new ServletHolder(new DataViewServlet(this)), "/stack"); + + ResourceHandler resources = new ResourceHandler(); + String filesDir = WarmRoast.class.getResource("/www").toExternalForm(); + resources.setResourceBase(filesDir); + resources.setDirectoriesListed(true); + resources.setWelcomeFiles(new String[]{ "index.html" }); + + HandlerList handlers = new HandlerList(); + handlers.addHandler(context); + handlers.addHandler(resources); + server.setHandler(handlers); + + server.start(); + server.join(); + } + + public static void main(String[] args) throws AgentLoadException { + RoastOptions opt = new RoastOptions(); + JCommander jc = new JCommander(opt, args); + jc.setProgramName("warmroast"); + + if (opt.help) { + jc.usage(); + System.exit(0); + } + + System.err.println(SEPARATOR); + System.err.println("WarmRoast"); + System.err.println("http://github.com/sk89q/warmroast"); + System.err.println(SEPARATOR); + System.err.println(""); + + VirtualMachine vm = null; + + if (opt.pid != null) { + try { + vm = VirtualMachine.attach(String.valueOf(opt.pid)); + System.err.println("Attaching to PID " + opt.pid + "..."); + } catch (AttachNotSupportedException | IOException e) { + System.err.println("Failed to attach VM by PID " + opt.pid); + System.exit(1); + } + } else if (opt.vmName != null) { + for (VirtualMachineDescriptor desc : VirtualMachine.list()) { + if (desc.displayName().contains(opt.vmName)) { + try { + vm = VirtualMachine.attach(desc); + System.err.println("Attaching to '" + desc.displayName() + "'..."); + + break; + } catch (AttachNotSupportedException | IOException e) { + System.err.println("Failed to attach VM by name '" + opt.vmName + "'"); + System.exit(1); + } + } + } + } + + if (vm == null) { + + List<VirtualMachineDescriptor> descriptors = VirtualMachine.list(); + System.err.println("Choose a VM:"); + + Collections.sort(descriptors, new Comparator<VirtualMachineDescriptor>() { + @Override + public int compare(VirtualMachineDescriptor o1, + VirtualMachineDescriptor o2) { + return o1.displayName().compareTo(o2.displayName()); + } + }); + + // Print list of VMs + int i = 1; + for (VirtualMachineDescriptor desc : descriptors) { + System.err.println("[" + (i++) + "] " + desc.displayName()); + } + + // Ask for choice + System.err.println(""); + System.err.print("Enter choice #: "); + BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); + String s; + try { + s = reader.readLine(); + } catch (IOException e) { + return; + } + + // Get the VM + try { + int choice = Integer.parseInt(s) - 1; + if (choice < 0 || choice >= descriptors.size()) { + System.err.println(""); + System.err.println("Given choice is out of range."); + System.exit(1); + } + vm = VirtualMachine.attach(descriptors.get(choice)); + } catch (NumberFormatException e) { + System.err.println(""); + System.err.println("That's not a number. Bye."); + System.exit(1); + } catch (AttachNotSupportedException | IOException e) { + System.err.println(""); + System.err.println("Failed to attach VM"); + System.exit(1); + } + } + + InetSocketAddress address = new InetSocketAddress(opt.bindAddress, opt.port); + + WarmRoast roast = new WarmRoast(vm, opt.interval); + if (opt.mappingsDir != null) { + File dir = new File(opt.mappingsDir); + File joined = new File(dir, "joined.srg"); + File methods = new File(dir, "methods.csv"); + try { + roast.getMapping().read(joined, methods); + } catch (IOException e) { + System.err.println( + "Failed to read the mappings files (joined.srg, methods.csv) " + + "from " + dir.getAbsolutePath() + ": " + e.getMessage()); + System.exit(2); + } + } + + roast.setFilterThread(opt.threadName); + + System.err.println(SEPARATOR); + + System.err.println("Starting a server on " + address.toString() + "..."); + System.err.println("Once the server starts (shortly), visit the URL in your browser."); + + try { + roast.connect(); + roast.start(address); + } catch (Throwable t) { + t.printStackTrace(); + System.exit(3); + } + } + +} diff --git a/src/main/resources/www/index.html b/src/main/resources/www/index.html new file mode 100644 index 0000000..93ecfb4 --- /dev/null +++ b/src/main/resources/www/index.html @@ -0,0 +1,13 @@ +<!DOCTYPE html><html><head><title>WarmRoast</title> +<style>@import url(style.css);</style> +</head><body> +<h1>WarmRoast</h1> + +<p> + <a href="/stack">View sampler results</a> +</p> + +<p class="footer"> +<a href="http://github.com/sk89q/warmroast">github.com/sk89q/warmroast</a></p> + +</body></html> diff --git a/src/main/resources/www/style.css b/src/main/resources/www/style.css new file mode 100644 index 0000000..45cfe2d --- /dev/null +++ b/src/main/resources/www/style.css @@ -0,0 +1,166 @@ +@import url(http://fonts.googleapis.com/css?family=Lato); + +body { + font-family: 'Lato', Arial, sans-serif; + font-size: 10pt; + line-height: 150%; + margin: 0; + padding: 54px 20px 20px 20px; +} + +ul { + margin: 0; + padding: 0 0 0 18px; +} + +li { + margin: 0; + margin-left: -10px; + padding: 0; + list-style: none; + border-left: 1px solid #ccc; +} + +a:link, a:visited { + color: #FF3213; + text-decoration: none; + border-bottom: 1px solid #CCC; +} + +a:hover, a:active { + color: #000; + border-color: black; + text-decoration: none; +} + +.stack { + margin-left: 60px; +} + +.name { + background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA10lEQVR4Xt2Tu8rCQBSEzzlJmYBvYJ7EShArfQ1BrPNDyoB5DRHsrUQQQc3lcfxBiKbIHjfiCuayKSwEB6ZYPpjdGVhkZvhEJP3dALMMgmB+FoI7ddUQEYhw7bp/48YAkYtOfzCEJu22m5G2gnjeHIYxxHGiXJwV11dQTzdN4w0QKa4JULplGcymkwo4Rkn7iCjNOcNiuVK3vQZ0ug5gWwAgPnpalgW1+yDqA4hQmmSADVVGBW8bES7RaW+zYOBSNSSENL0etAGe5/UkMKBZ/77vv8APfKY7cvZVTt7VqzwAAAAASUVORK5CYII=) center left no-repeat; + padding-left: 20px; + cursor: pointer; +} + +.name:hover { + background-color: #CCC; +} + +.name:hover + ul { + background: #EFEFEF; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4); + border-radius: 3px; +} + +.matched { + background: #CCC; + border-radius: 3px; + padding: 0 4px; +} + +.multiple-matches { + background: #FF3213; + color: #FFF; + padding: 0 4px; + border-radius: 3px; +} + +.matched:hover, .multiple-matches:hover { + background: #000000; + color: #FFF; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4); +} + +.percent { + color: #6b98ff; + font-size: 90%; + border-radius: 3px; + padding: 0 4px; +} + +.bar { + display: inline-block; + width: 100px; + height: 15px; + margin-left: 20px; + border: 1px solid #CCC; + position: absolute; + right: 30px; + background: #FFF; +} + +.bar-inner { + display: inline-block; + height: 16px; + background: #6b98ff; +} + +#overlay span { + position: absolute; + color: #6b98ff; + font-size: 90%; + z-index: 10; + line-height: 150%; + left: 20px; + width: 50px; + text-align: right; +} + +.time { + display: none; + margin: 0; + color: #888; + font-size: 90%; + border-radius: 3px; + padding: 0 4px; +} + +.name:hover .time { + display: inline; +} + +ul { + display: none; +} + +.collapsed > .name { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA7ElEQVR4Xt1TzYrCQAxOYo8z0GeRZY+eBPGkryGIZ4UeC/Yx/EHvnqTssot/9XF2YUHXQyd2RtuC2OnBg2AgJF/CfJkvwyAzwyNGiT+XwLktBMHwRyl270lDRCDCRb8/aBcSqFi59UYTiuwzXLasEtR18nYbQRTt4f2tqqPGad8uIb2641TyKUSJp307gbH/0wl63U4Ks3y925e/AibOMcN4OofRZKZLOmps6lj2CoBodAohspKUMtePaCcgQqNZCGlw+PFt8nwXWLZE+NttviQrBr6RhoRwOBxXVgLP82pJqECx/fq+n4EX+ExnBI9csQQ1hIoAAAAASUVORK5CYII=); +} + +h1 { + background: #FFF; + color: #111; + position: fixed; + top: 0; + left: 0; + right: 0; + padding: 10px 20px; + margin: 0; + font-size: 14pt; + font-weight: normal; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.4); + z-index: 20; +} + +.footer { + background: #FFF; + color: #333; + margin: 100px 0 0 0; + font-size: 10pt; + font-weight: normal; + text-align: right; +} + +.loading { + font-size: 130%; + background: #EFEFEF; + border: 1px solid #CCC; + padding: 8px; + border-radius: 3px; +} + +.no-results { + font-size: 130%; + color: #800000; +}
\ No newline at end of file diff --git a/src/main/resources/www/warmroast.js b/src/main/resources/www/warmroast.js new file mode 100644 index 0000000..c7dd7bc --- /dev/null +++ b/src/main/resources/www/warmroast.js @@ -0,0 +1,43 @@ +$(".name").on("click", function(event) { + var $parent = $(this).parent(); + if ($parent.hasClass("collapsed")) { + $parent.removeClass("collapsed"); + $parent.children("ul").slideDown(50); + } else { + $parent.addClass("collapsed"); + $parent.children("ul").slideUp(50); + } +}); + +function extractTime($el) { + var text = $el.children(".name") + .children(".time").text().replace(/[^0-9]/, ""); + return parseInt(text); +} + +var $overlay = $("#overlay"); + +$(".name").on("mouseenter", function(event) { + var $this = $(this); + var thisTime = null; + $overlay.empty(); + $this.parents(".node").each(function(i, parent) { + var $parent = $(parent); + var time = extractTime($parent); + if (thisTime == null) { + thisTime = time; + } else { + var $el = $(document.createElement("span")); + var pos = $parent.position(); + var width = $el.outerWidth(); + $el.text(((thisTime / time) * 100).toFixed(2) + "%"); + $el.css({ + top: pos.top + "px" + }); + $overlay.append($el); + } + }); +}); + +$(".loading").hide(); +$(".stack").show();
\ No newline at end of file |