aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlbert Pham <the.sk89q@gmail.com>2013-06-13 15:55:12 -0700
committerAlbert Pham <the.sk89q@gmail.com>2013-06-13 15:55:12 -0700
commit280ad8b8bf2942f055daf5fb6f8ae55a7f88243a (patch)
tree05b6fa56f6b826242f6a42f1f8fb2514e3d568d9
downloadspark-280ad8b8bf2942f055daf5fb6f8ae55a7f88243a.tar.gz
spark-280ad8b8bf2942f055daf5fb6f8ae55a7f88243a.tar.bz2
spark-280ad8b8bf2942f055daf5fb6f8ae55a7f88243a.zip
Initial commit.
-rw-r--r--.gitignore32
-rw-r--r--README.md77
-rw-r--r--pom.xml145
-rw-r--r--src/main/java/com/sk89q/warmroast/ClassMapping.java67
-rw-r--r--src/main/java/com/sk89q/warmroast/DataViewServlet.java76
-rw-r--r--src/main/java/com/sk89q/warmroast/McpMapping.java112
-rw-r--r--src/main/java/com/sk89q/warmroast/RoastOptions.java49
-rw-r--r--src/main/java/com/sk89q/warmroast/StackNode.java162
-rw-r--r--src/main/java/com/sk89q/warmroast/StackTraceNode.java96
-rw-r--r--src/main/java/com/sk89q/warmroast/WarmRoast.java309
-rw-r--r--src/main/resources/www/index.html13
-rw-r--r--src/main/resources/www/style.css166
-rw-r--r--src/main/resources/www/warmroast.js43
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).
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..b96ca02
--- /dev/null
+++ b/pom.xml
@@ -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> &mdash; ");
+ 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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
+ }
+
+}
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