Repository: sk89q/WarmRoast
Branch: master
Commit: d814c4f03915
Files: 14
Total size: 43.7 KB
Directory structure:
gitextract_dl1f2tde/
├── .github/
│ └── workflows/
│ └── build.yml
├── .gitignore
├── README.md
├── pom.xml
└── src/
└── main/
├── java/
│ └── com/
│ └── sk89q/
│ └── warmroast/
│ ├── ClassMapping.java
│ ├── DataViewServlet.java
│ ├── McpMapping.java
│ ├── RoastOptions.java
│ ├── StackNode.java
│ ├── StackTraceNode.java
│ └── WarmRoast.java
└── resources/
└── www/
├── index.html
├── style.css
└── warmroast.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/build.yml
================================================
name: Build
on:
- push
- pull_request
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
name: Checkout repo
- name: Set up JDK 17 (LTS)
uses: actions/setup-java@v2
with:
java-version: '17'
distribution: 'adopt'
cache: maven
- name: Build with Maven
run: mvn install
- uses: actions/upload-artifact@v2
name: Upload Artifact
with:
name: WarmRoast
path: target/warmroast-*.jar
================================================
FILE: .gitignore
================================================
# 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/
================================================
FILE: README.md
================================================
WarmRoast
=========
*Note: This project is not actively maintained but should work. (2024)*
WarmRoast attaches to Minecraft (or any Java application) and lets you see what it's doing. While what it's doing can be cryptic, with some practice, you can start to figure out patterns.
* 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 Release**: [here](../../releases)
**Latest Build**: [here](../../actions/workflows/build.yml)
Screenshots
-----------

Usage
-----
Extract the .zip file and place the .jar somewhere.
### For Java 9 and newer ###
The `tools.jar` is automatically included into JDK's since Java 9. You only should use something like this:
java -cp warmroast-1.0.0-SNAPSHOT.jar com.sk89q.warmroast.WarmRoast --thread "Server thread"
### For Java 7 & 8 ###
1. Note the path of your JDK.
2. Download WarmRoast.
3. Replace `PATH_TO_JDK` in the following commands with the path to your JDK and execute the program.
**Note:** The example command line below includes `--thread "Server thread"`, which filters all threads but the main server thread. You can remove it to show all threads.
**Modded/vanilla servers:** If you are using a modded server, get a copy of [MCP](http://mcp.ocean-labs.de/index.php/MCP_Releases) for your server's Minecraft version, copy the files from conf/ somewhere, and point WarmRoast to it with `--mappings path/to/folder`. This helps readability a lot. Bukkit uses its own mapping, so a pure non-modded Bukkit server can't use MCP mappings.
#### Linux ####
java -Djava.library.path=PATH_TO_JDK/jre/bin -cp PATH_TO_JDK/lib/tools.jar:warmroast-1.0.0-SNAPSHOT.jar com.sk89q.warmroast.WarmRoast --thread "Server thread"
#### Windows ####
An example `PATH_TO_JDK` would be `C:\Program Files\Java\jdk1.7.0_45`
java -Djava.library.path=PATH_TO_JDK/jre/bin -cp PATH_TO_JDK/lib/tools.jar;warmroast-1.0.0-SNAPSHOT.jar com.sk89q.warmroast.WarmRoast --thread "Server thread"
* The folder `PATH_TO_JDK/jre/bin` should contain "attach.dll"
* The folder `PATH_TO_JDK/lib` should contain "tools.jar"
Parameters
----------
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
--timeout
The number of seconds before ceasing sampling (optional)
Hint: `--thread "Server thread"` is useful for Minecraft servers.
License
-------
The project is licensed under the GNU General Public License, version 3.
================================================
FILE: pom.xml
================================================
<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>
<distributionManagement>
<site>
<id>sk89q-docs-upload</id>
<url>ftp://sk89q-maven-deploy/worldedit/</url>
</site>
<repository>
<id>maven.sk89q.com</id>
<url>http://maven.sk89q.com/artifactory/libs-release-local</url>
</repository>
<snapshotRepository>
<id>maven.sk89q.com-snapshot</id>
<url>http://maven.sk89q.com/artifactory/libs-snapshot-local</url>
</snapshotRepository>
</distributionManagement>
<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>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
================================================
FILE: src/main/java/com/sk89q/warmroast/ClassMapping.java
================================================
/*
* 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();
}
}
================================================
FILE: src/main/java/com/sk89q/warmroast/DataViewServlet.java
================================================
/*
* 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>");
}
}
================================================
FILE: src/main/java/com/sk89q/warmroast/McpMapping.java
================================================
/*
* 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 mapMethodId(String id) {
return methods.get(id);
}
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));
}
}
}
}
}
================================================
FILE: src/main/java/com/sk89q/warmroast/RoastOptions.java
================================================
/*
* 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;
@Parameter(names = { "--timeout" }, description = "The number of seconds before ceasing sampling (optional)")
public Integer timeout;
}
================================================
FILE: src/main/java/com/sk89q/warmroast/StackNode.java
================================================
/*
* 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.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
public class StackNode implements Comparable<StackNode> {
private static final NumberFormat cssDec = NumberFormat.getPercentInstance(Locale.US);
private final String name;
private final Map<String, StackNode> children = new HashMap<>();
private long totalTime;
static {
cssDec.setGroupingUsed(false);
cssDec.setMaximumFractionDigits(2);
}
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(formatCssPct(getTotalTime() / (double) totalTime))
.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 formatCssPct(double pct) {
return cssDec.format(pct);
}
protected static String escapeHtml(String str) {
return str.replace("&", "&").replace("<", "<").replace(">", ">");
}
}
================================================
FILE: src/main/java/com/sk89q/warmroast/StackTraceNode.java
================================================
/*
* 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.mapMethodId(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;
}
}
}
================================================
FILE: src/main/java/com/sk89q/warmroast/WarmRoast.java
================================================
/*
* 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;
private long endTime = -1;
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 long getEndTime() {
return endTime;
}
public void setEndTime(long l) {
this.endTime = l;
}
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() {
if (endTime >= 0) {
if (endTime <= System.currentTimeMillis()) {
cancel();
System.err.println("Sampling has stopped.");
return;
}
}
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);
e.printStackTrace();
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 + "'");
e.printStackTrace();
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");
e.printStackTrace();
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);
}
}
System.err.println(SEPARATOR);
roast.setFilterThread(opt.threadName);
if (opt.timeout != null && opt.timeout > 0) {
roast.setEndTime(System.currentTimeMillis() + opt.timeout * 1000);
System.err.println("Sampling set to stop in " + opt.timeout + " seconds.");
}
System.err.println("Starting a server on " + address.toString() + "...");
System.err.println("Once the server starts (shortly), visit the URL in your browser.");
System.err.println("Note: The longer you wait before using the output of that " +
"webpage, the more accurate the results will be.");
try {
roast.connect();
roast.start(address);
} catch (Throwable t) {
t.printStackTrace();
System.exit(3);
}
}
}
================================================
FILE: src/main/resources/www/index.html
================================================
<!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>
================================================
FILE: src/main/resources/www/style.css
================================================
@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;
}
================================================
FILE: src/main/resources/www/warmroast.js
================================================
$(".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();
gitextract_dl1f2tde/
├── .github/
│ └── workflows/
│ └── build.yml
├── .gitignore
├── README.md
├── pom.xml
└── src/
└── main/
├── java/
│ └── com/
│ └── sk89q/
│ └── warmroast/
│ ├── ClassMapping.java
│ ├── DataViewServlet.java
│ ├── McpMapping.java
│ ├── RoastOptions.java
│ ├── StackNode.java
│ ├── StackTraceNode.java
│ └── WarmRoast.java
└── resources/
└── www/
├── index.html
├── style.css
└── warmroast.js
SYMBOL INDEX (58 symbols across 8 files)
FILE: src/main/java/com/sk89q/warmroast/ClassMapping.java
class ClassMapping (line 26) | public class ClassMapping {
method ClassMapping (line 32) | public ClassMapping(String obfuscated, String actual) {
method getObfuscated (line 37) | public String getObfuscated() {
method getActual (line 41) | public String getActual() {
method addMethod (line 45) | public void addMethod(String obfuscated, String actual) {
method mapMethod (line 54) | public List<String> mapMethod(String obfuscated) {
method toString (line 62) | @Override
FILE: src/main/java/com/sk89q/warmroast/DataViewServlet.java
class DataViewServlet (line 30) | public class DataViewServlet extends HttpServlet {
method DataViewServlet (line 36) | public DataViewServlet(WarmRoast roast) {
method doGet (line 40) | @Override
FILE: src/main/java/com/sk89q/warmroast/McpMapping.java
class McpMapping (line 34) | public class McpMapping {
method mapClass (line 45) | public ClassMapping mapClass(String obfuscated) {
method read (line 49) | public void read(File joinedFile, File methodsFile) throws IOException {
method mapMethodId (line 62) | public String mapMethodId(String id) {
method fromMethodId (line 66) | public String fromMethodId(String id) {
method processMethodNames (line 74) | private void processMethodNames(List<String[]> entries) {
method processClasses (line 88) | private void processClasses(List<String> lines) {
method processMethods (line 99) | private void processMethods(List<String> lines) {
FILE: src/main/java/com/sk89q/warmroast/RoastOptions.java
class RoastOptions (line 23) | public class RoastOptions {
FILE: src/main/java/com/sk89q/warmroast/StackNode.java
class StackNode (line 30) | public class StackNode implements Comparable<StackNode> {
method StackNode (line 42) | public StackNode(String name) {
method getName (line 46) | public String getName() {
method getNameHtml (line 50) | public String getNameHtml(McpMapping mapping) {
method getChildren (line 54) | public Collection<StackNode> getChildren() {
method getChild (line 60) | public StackNode getChild(String name) {
method getChild (line 69) | public StackNode getChild(String className, String methodName) {
method getTotalTime (line 79) | public long getTotalTime() {
method log (line 83) | public void log(long time) {
method log (line 87) | private void log(StackTraceElement[] elements, int skip, long time) {
method log (line 99) | public void log(StackTraceElement[] elements, long time) {
method compareTo (line 103) | @Override
method writeHtml (line 108) | private void writeHtml(StringBuilder builder, McpMapping mapping, long...
method toHtml (line 137) | public String toHtml(McpMapping mapping) {
method writeString (line 143) | private void writeString(StringBuilder builder, int indent) {
method toString (line 159) | @Override
method formatCssPct (line 166) | protected static String formatCssPct(double pct) {
method escapeHtml (line 170) | protected static String escapeHtml(String str) {
FILE: src/main/java/com/sk89q/warmroast/StackTraceNode.java
class StackTraceNode (line 23) | public class StackTraceNode extends StackNode {
method StackTraceNode (line 28) | public StackTraceNode(String className, String methodName) {
method getClassName (line 34) | public String getClassName() {
method getMethodName (line 38) | public String getMethodName() {
method getNameHtml (line 42) | @Override
method compareTo (line 85) | @Override
FILE: src/main/java/com/sk89q/warmroast/WarmRoast.java
class WarmRoast (line 59) | public class WarmRoast extends TimerTask {
method WarmRoast (line 75) | public WarmRoast(VirtualMachine vm, int interval) {
method getData (line 80) | public Map<String, StackNode> getData() {
method getNode (line 84) | private StackNode getNode(String name) {
method getMapping (line 93) | public McpMapping getMapping() {
method getFilterThread (line 97) | public String getFilterThread() {
method setFilterThread (line 101) | public void setFilterThread(String filterThread) {
method getEndTime (line 105) | public long getEndTime() {
method setEndTime (line 109) | public void setEndTime(long l) {
method connect (line 113) | public void connect()
method getThreadMXBean (line 138) | private ThreadMXBean getThreadMXBean()
method run (line 149) | @Override
method start (line 177) | public void start(InetSocketAddress address) throws Exception {
method main (line 201) | public static void main(String[] args) throws AgentLoadException {
FILE: src/main/resources/www/warmroast.js
function extractTime (line 12) | function extractTime($el) {
Condensed preview — 14 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (48K chars).
[
{
"path": ".github/workflows/build.yml",
"chars": 522,
"preview": "name: Build\n\non:\n - push\n - pull_request\n\njobs:\n build:\n name: Build\n runs-on: ubuntu-latest\n\n steps:\n - "
},
{
"path": ".gitignore",
"chars": 303,
"preview": "# Eclipse stuff\n/.classpath\n/.project\n/.settings\n\n# netbeans\n/nbproject\n\n# we use maven!\n/build.xml\n\n# maven\n/target\n\n# "
},
{
"path": "README.md",
"chars": 3580,
"preview": "WarmRoast\n=========\n\n*Note: This project is not actively maintained but should work. (2024)*\n\nWarmRoast attaches to Mine"
},
{
"path": "pom.xml",
"chars": 3526,
"preview": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocat"
},
{
"path": "src/main/java/com/sk89q/warmroast/ClassMapping.java",
"chars": 1878,
"preview": "/*\n * WarmRoast\n * Copyright (C) 2013 Albert Pham <http://www.sk89q.com>\n *\n * This program is free software: you can re"
},
{
"path": "src/main/java/com/sk89q/warmroast/DataViewServlet.java",
"chars": 3137,
"preview": "/*\n * WarmRoast\n * Copyright (C) 2013 Albert Pham <http://www.sk89q.com>\n *\n * This program is free software: you can re"
},
{
"path": "src/main/java/com/sk89q/warmroast/McpMapping.java",
"chars": 3866,
"preview": "/*\n * WarmRoast\n * Copyright (C) 2013 Albert Pham <http://www.sk89q.com>\n *\n * This program is free software: you can re"
},
{
"path": "src/main/java/com/sk89q/warmroast/RoastOptions.java",
"chars": 1948,
"preview": "/*\n * WarmRoast\n * Copyright (C) 2013 Albert Pham <http://www.sk89q.com>\n *\n * This program is free software: you can re"
},
{
"path": "src/main/java/com/sk89q/warmroast/StackNode.java",
"chars": 5503,
"preview": "/*\n * WarmRoast\n * Copyright (C) 2013 Albert Pham <http://www.sk89q.com>\n *\n * This program is free software: you can re"
},
{
"path": "src/main/java/com/sk89q/warmroast/StackTraceNode.java",
"chars": 3463,
"preview": "/*\n * WarmRoast\n * Copyright (C) 2013 Albert Pham <http://www.sk89q.com>\n *\n * This program is free software: you can re"
},
{
"path": "src/main/java/com/sk89q/warmroast/WarmRoast.java",
"chars": 12188,
"preview": "/*\n * WarmRoast\n * Copyright (C) 2013 Albert Pham <http://www.sk89q.com>\n *\n * This program is free software: you can re"
},
{
"path": "src/main/resources/www/index.html",
"chars": 295,
"preview": "<!DOCTYPE html><html><head><title>WarmRoast</title>\n<style>@import url(style.css);</style>\n</head><body>\n<h1>WarmRoast</"
},
{
"path": "src/main/resources/www/style.css",
"chars": 3374,
"preview": "@import url(http://fonts.googleapis.com/css?family=Lato);\n\nbody {\n font-family: 'Lato', Arial, sans-serif;\n font-s"
},
{
"path": "src/main/resources/www/warmroast.js",
"chars": 1199,
"preview": "$(\".name\").on(\"click\", function(event) {\n var $parent = $(this).parent();\n if ($parent.hasClass(\"collapsed\")) {\n "
}
]
About this extraction
This page contains the full source code of the sk89q/WarmRoast GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 14 files (43.7 KB), approximately 11.1k tokens, and a symbol index with 58 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.