diff --git a/pom.xml b/pom.xml
index 0846d5f..858713e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,13 +5,13 @@
4.0.0
de.steamwar
- AttachTool
+ LixfelsProfiler
1.0
UTF-8
- 16
- 16
+ 11
+ 11
@@ -24,11 +24,16 @@
true
- de.steamwar.AttachTool
+ de.steamwar.Main
+
+ de.steamwar.Agent
+ de.steamwar.Agent
+
+ LixfelsProfiler
\ No newline at end of file
diff --git a/src/main/java/de/steamwar/Agent.java b/src/main/java/de/steamwar/Agent.java
new file mode 100644
index 0000000..025854c
--- /dev/null
+++ b/src/main/java/de/steamwar/Agent.java
@@ -0,0 +1,29 @@
+package de.steamwar;
+
+import java.lang.instrument.Instrumentation;
+
+public class Agent {
+ private Agent() {}
+
+ private static Sampler sampler;
+
+ public static void premain(String args, Instrumentation inst) {
+ if ("start".equals(args)) {
+ sampler = new Sampler(true);
+ }
+ }
+
+ public static void agentmain(String args, Instrumentation inst) {
+ switch (args) {
+ case "start":
+ if(sampler == null)
+ sampler = new Sampler(false);
+ break;
+ case "stop":
+ if(sampler != null)
+ sampler.stop();
+ sampler = null;
+ break;
+ }
+ }
+}
diff --git a/src/main/java/de/steamwar/AttachTool.java b/src/main/java/de/steamwar/AttachTool.java
deleted file mode 100644
index 6c67a87..0000000
--- a/src/main/java/de/steamwar/AttachTool.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package de.steamwar;
-
-import com.sun.tools.attach.AttachNotSupportedException;
-import com.sun.tools.attach.VirtualMachine;
-import com.sun.tools.attach.VirtualMachineDescriptor;
-import com.sun.tools.attach.spi.AttachProvider;
-
-import java.io.IOException;
-import java.util.Properties;
-
-public class AttachTool {
-
- public static void main(String[] args) {
- if(args.length == 0) {
- for(AttachProvider provider : AttachProvider.providers()) {
- for(VirtualMachineDescriptor vmd : provider.listVirtualMachines()) {
- System.out.println(vmd.id() + " " + vmd.displayName());
- }
- }
- }else if(args.length == 1) {
- try {
- VirtualMachine vm = VirtualMachine.attach(args[0]);
-
- // start management agent
- Properties props = new Properties();
- props.put("com.sun.management.jmxremote.port", "1999");
- props.put("com.sun.management.jmxremote.authenticate", "false");
- props.put("com.sun.management.jmxremote.ssl", "false");
- vm.startManagementAgent(props);
-
- vm.detach();
- } catch (AttachNotSupportedException | IOException e) {
- e.printStackTrace();
- return;
- }
-
- }
- }
-}
diff --git a/src/main/java/de/steamwar/Main.java b/src/main/java/de/steamwar/Main.java
new file mode 100644
index 0000000..319b4c6
--- /dev/null
+++ b/src/main/java/de/steamwar/Main.java
@@ -0,0 +1,51 @@
+package de.steamwar;
+
+import com.sun.tools.attach.*;
+import com.sun.tools.attach.spi.AttachProvider;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URISyntaxException;
+
+public class Main {
+
+ public static final File jarPath;
+
+ static {
+ File jarPath1;
+ try {
+ jarPath1 = new File(Main.class.getProtectionDomain().getCodeSource().getLocation().toURI());
+ } catch (URISyntaxException e) {
+ jarPath1 = null;
+ }
+ jarPath = jarPath1;
+ }
+
+ public static void main(String[] args) {
+ if(args.length == 0) {
+ for(AttachProvider provider : AttachProvider.providers()) {
+ for(VirtualMachineDescriptor vmd : provider.listVirtualMachines()) {
+ System.out.println(vmd.id() + " " + vmd.displayName());
+ }
+ }
+ }else if(args.length == 1) {
+ try {
+ VirtualMachine vm = VirtualMachine.attach(args[0]);
+
+ vm.loadAgent(jarPath.getAbsolutePath(), "start");
+
+ System.out.println("Press keyboard to stop sampling...");
+ System.in.read();
+
+ vm.loadAgent(jarPath.getAbsolutePath(), "stop");
+
+ vm.detach();
+
+ new ProcessBuilder("xdot", "samples.dot").directory(jarPath.getParentFile()).start();
+ } catch (AttachNotSupportedException | IOException | AgentLoadException | AgentInitializationException e) {
+ e.printStackTrace();
+ }
+
+ }
+ }
+}
diff --git a/src/main/java/de/steamwar/Sampler.java b/src/main/java/de/steamwar/Sampler.java
new file mode 100644
index 0000000..bdf2116
--- /dev/null
+++ b/src/main/java/de/steamwar/Sampler.java
@@ -0,0 +1,134 @@
+package de.steamwar;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.util.*;
+import java.util.concurrent.locks.LockSupport;
+
+public class Sampler {
+
+ public static final long SAMPLING_SPEED = 1000000;
+ public static final double FILTER = 0.005;
+
+ private static final Set waitingMethods = new HashSet<>();
+ static {
+ waitingMethods.add("io.netty.channel.epoll.Native.epollWait");
+ waitingMethods.add("org.jline.utils.NonBlockingInputStream.read");
+ waitingMethods.add("org.bukkit.craftbukkit.libs.jline.internal.NonBlockingInputStream.read");
+ waitingMethods.add("org.bukkit.craftbukkit.v1_15_R1.util.TerminalConsoleWriterThread.run");
+ waitingMethods.add("sun.nio.ch.SocketDispatcher.read0");
+ waitingMethods.add("openj9.internal.tools.attach.target.IPC.waitSemaphore");
+ }
+
+ private static final List omittedMethods = new ArrayList<>();
+ static {
+ omittedMethods.add("java.lang.reflect.Method.invoke");
+ omittedMethods.add("java.lang.Thread.run");
+ omittedMethods.add("jdk.internal");
+ }
+
+ private final Map traces = new HashMap<>();
+ private final Thread samplerThread;
+ private final boolean showDot;
+ private boolean shutdown = false;
+
+ private int sampleRuns;
+
+ public Sampler(boolean showDot) {
+ this.showDot = showDot;
+ samplerThread = new Thread(this::run, "Sampler");
+ samplerThread.setDaemon(true);
+ samplerThread.start();
+ }
+
+ public int getSampleRuns() {
+ return sampleRuns;
+ }
+
+ public void stop() {
+ shutdown = true;
+ try {
+ samplerThread.join();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+
+ File output = new File(Main.jarPath.getParentFile(), "samples.dot");
+ try {
+ OutputStreamWriter writer = new FileWriter(output);
+ toDot(writer);
+ writer.flush();
+ writer.close();
+ if(showDot)
+ new ProcessBuilder("xdot", "samples.dot").directory(Main.jarPath.getParentFile()).start();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void run() {
+ Thread autoStopper = new Thread(this::stop, "SamplerStop");
+ Runtime.getRuntime().addShutdownHook(autoStopper);
+ long lastTime = System.nanoTime();
+ while(!shutdown) {
+ sample();
+
+ long currentTime = System.nanoTime();
+ LockSupport.parkNanos(SAMPLING_SPEED - currentTime + lastTime);
+ lastTime = currentTime;
+ }
+
+ try {
+ Runtime.getRuntime().removeShutdownHook(autoStopper);
+ } catch (IllegalStateException e) {
+ // ignored
+ }
+ }
+
+ private void sample() {
+ sampleRuns++;
+
+ Map stackTraces = Thread.getAllStackTraces();
+ List filteredStack = new LinkedList<>();
+
+ for(Map.Entry entry : stackTraces.entrySet()) {
+ Thread thread = entry.getKey();
+ StackTraceElement[] stack = entry.getValue();
+ if(stack.length == 0 || thread.getName().equals("Sampler"))
+ continue;
+
+ Thread.State state = thread.getState();
+
+ filteredStack.clear();
+ for(StackTraceElement ste : stack) {
+ String id = ste.getClassName() + "." + ste.getMethodName();
+ if (waitingMethods.contains(id))
+ state = Thread.State.WAITING;
+ if(omittedMethods.stream().noneMatch(id::startsWith))
+ filteredStack.add(0, ste);
+ }
+
+ Trace predecessor = traces.computeIfAbsent(thread.getName(), name -> new Trace(this, thread));
+ predecessor.add(state, null);
+
+ for(StackTraceElement ste : filteredStack) {
+ Trace trace = traces.computeIfAbsent(ste.getClassName() + "." + ste.getMethodName() + ":" + ste.getLineNumber(), id -> new Trace(this, ste));
+ trace.add(state, predecessor);
+ predecessor = trace;
+ }
+ }
+ }
+
+ private void toDot(OutputStreamWriter writer) throws IOException {
+ writer.append("digraph {\nnode [shape=plaintext,style=\"filled\", margin=0.1];\n");
+
+ for(Trace trace : traces.values()) {
+ trace.toDot(writer);
+ }
+
+ writer.append("}");
+ }
+
+}
diff --git a/src/main/java/de/steamwar/Trace.java b/src/main/java/de/steamwar/Trace.java
new file mode 100644
index 0000000..bc16ab1
--- /dev/null
+++ b/src/main/java/de/steamwar/Trace.java
@@ -0,0 +1,81 @@
+package de.steamwar;
+
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.text.DecimalFormat;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.Map;
+
+public class Trace {
+ private static final DecimalFormat df = new DecimalFormat("0.00");
+
+ private static int idCounter;
+
+ private final Sampler sampler;
+ private final int id;
+ private final String name;
+
+ private final Map samples = new EnumMap<>(Thread.State.class);
+ private final Map predecessors = new HashMap<>();
+
+ public Trace(Sampler sampler, StackTraceElement ste) {
+ this.sampler = sampler;
+ id = idCounter++;
+ name = ste.getClassName() + "." + ste.getMethodName() + ":" + ste.getLineNumber();
+ }
+
+ public Trace(Sampler sampler, Thread thread) {
+ this(sampler, thread.getName());
+ }
+
+ public Trace(Sampler sampler, String name) {
+ this.sampler = sampler;
+ id = idCounter++;
+ this.name = name;
+ }
+
+ private int ownSampleRuns() {
+ return samples.values().stream().mapToInt(i -> i).sum();
+ }
+
+ private boolean filtered() {
+ return (samples.getOrDefault(Thread.State.RUNNABLE, 0) + samples.getOrDefault(Thread.State.BLOCKED, 0)) / (double) sampler.getSampleRuns() < Sampler.FILTER;
+ }
+
+ public void add(Thread.State state, Trace predecessor) {
+ samples.compute(state, (s, sample) -> sample == null ? 1 : sample + 1);
+ if (predecessor != null)
+ predecessors.compute(predecessor, (pre, sample) -> sample == null ? 1 : sample + 1);
+ }
+
+ private String percentage(int value) {
+ return df.format(value * 100.0 / sampler.getSampleRuns());
+ }
+
+ public void toDot(OutputStreamWriter writer) throws IOException {
+ if (filtered())
+ return;
+
+ int waiting = samples.getOrDefault(Thread.State.WAITING, 0) + samples.getOrDefault(Thread.State.TIMED_WAITING, 0) + samples.getOrDefault(Thread.State.TERMINATED, 0);
+ int runnable = samples.getOrDefault(Thread.State.RUNNABLE, 0) + samples.getOrDefault(Thread.State.NEW, 0);
+ int blocked = samples.getOrDefault(Thread.State.BLOCKED, 0);
+ int total = waiting + runnable + blocked;
+
+ int r = (255 * blocked + 192 * waiting) / total;
+ int g = (255 * runnable + 192 * waiting) / total;
+ int b = (192 * waiting) / total;
+ int a = (int) (255 * Math.sqrt(total / (double) sampler.getSampleRuns()));
+ if (a > 255)
+ a = 255;
+
+ writer.append(String.valueOf(id)).append(" [fillcolor=\"#").append(String.format("%02X", r)).append(String.format("%02X", g)).append(String.format("%02X", b)).append(String.format("%02X", a)).append("\",label=\"").append(name).append("\\n").append(percentage(ownSampleRuns())).append("% ").append(df.format(ownSampleRuns() * Sampler.SAMPLING_SPEED / 1e9)).append("s\\nR").append(percentage(runnable)).append("% B").append(percentage(blocked)).append("% W").append(percentage(waiting)).append("%\"];\n");
+
+ for (Map.Entry entry : predecessors.entrySet()) {
+ if (entry.getKey().filtered() || entry.getValue() / (double)sampler.getSampleRuns() < Sampler.FILTER)
+ continue;
+
+ writer.append(String.valueOf(entry.getKey().id)).append(" -> ").append(String.valueOf(id)).append(" [label=\"").append(percentage(entry.getValue())).append("%\"];\n");
+ }
+ }
+}