diff --git a/.gitignore b/.gitignore
index f419dc7..5c33c30 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
/.idea
/target
+/dependency-reduced-pom.xml
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 858713e..cca24b1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -10,30 +10,58 @@
UTF-8
- 11
- 11
+ 8
+ 8
org.apache.maven.plugins
- 3.2.0
- maven-jar-plugin
-
-
-
- true
- de.steamwar.Main
-
-
- de.steamwar.Agent
- de.steamwar.Agent
-
-
-
+ maven-shade-plugin
+ 3.2.4
+
+
+ package
+
+ shade
+
+
+ true
+
+
+
+ de.steamwar.Main
+ de.steamwar.Agent
+ de.steamwar.Agent
+
+
+
+
+
+
LixfelsProfiler
+
+
+
+ fabric
+ https://maven.fabricmc.net/
+
+
+
+
+
+ tools.profiler
+ async-profiler
+ 2.9
+
+
+ net.fabricmc
+ mapping-io
+ 0.3.0
+
+
\ No newline at end of file
diff --git a/src/main/java/de/steamwar/Agent.java b/src/main/java/de/steamwar/Agent.java
index 6031d87..ad8324d 100644
--- a/src/main/java/de/steamwar/Agent.java
+++ b/src/main/java/de/steamwar/Agent.java
@@ -1,32 +1,68 @@
package de.steamwar;
+import one.profiler.AsyncProfiler;
+
+import java.io.IOException;
import java.lang.instrument.Instrumentation;
public class Agent {
private Agent() {}
- private static Sampler sampler;
+ private static Thread shutdownHook;
+ private static Instrumentation instrumentation;
+ public static Instrumentation getInstrumentation() {
+ return instrumentation;
+ }
public static void premain(String args, Instrumentation inst) {
- if ("start".equals(args)) {
- sampler = new Sampler();
- }
+ agentmain(args, inst);
}
public static void agentmain(String args, Instrumentation inst) {
+ instrumentation = inst;
+ //AsyncProfiler.getInstance("/home/lixfel/libasyncProfiler.so");
switch (args) {
case "start":
- if(sampler == null)
- sampler = new Sampler();
+ start();
break;
case "stop":
- if(sampler != null)
- sampler.stop();
- sampler = null;
+ stop();
break;
case "heap":
OpenJ9.heapdump();
break;
}
}
+
+ private static void start() {
+ if(shutdownHook != null)
+ return;
+
+ callProfiler("start,event=wall,threads,interval=1000000,cstack=fp"); // 1mio ns = 1ms
+ shutdownHook = new Thread(Agent::stop, "SamplerShutdownHook");
+ Runtime.getRuntime().addShutdownHook(shutdownHook);
+ }
+
+ private static void stop() {
+ if(shutdownHook == null)
+ return;
+
+ AsyncProfiler.getInstance().stop();
+ new Profile(ObfHelper.loadMappingsIfPresent(), callProfiler("collapsed=samples,dot,sig")).save();
+
+ try {
+ Runtime.getRuntime().removeShutdownHook(shutdownHook);
+ shutdownHook = null;
+ } catch (IllegalStateException e) {
+ //ignored
+ }
+ }
+
+ private static String callProfiler(String command) {
+ try {
+ return AsyncProfiler.getInstance().execute(command);
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
}
diff --git a/src/main/java/de/steamwar/Method.java b/src/main/java/de/steamwar/Method.java
new file mode 100644
index 0000000..1572d80
--- /dev/null
+++ b/src/main/java/de/steamwar/Method.java
@@ -0,0 +1,189 @@
+package de.steamwar;
+
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.text.DecimalFormat;
+import java.util.*;
+
+public class Method {
+ private static final DecimalFormat df = new DecimalFormat("0.00");
+
+ private static int idCounter;
+
+ private final Profile profile;
+ private final int id;
+ private final String name;
+
+ private final int[] samples = new int[State.values().length];
+ private final HashMap calls = new HashMap<>();
+
+ public Method(Profile profile, Map mappings, String name) {
+ this.profile = profile;
+ this.id = idCounter++;
+
+ try {
+ name = javaPrettify(mappings, name);
+ } catch (IllegalArgumentException e) {
+ if(name.startsWith("/")) { // native lib
+ name = name.substring(name.lastIndexOf('/') + 1);
+ }
+ }
+
+ this.name = name;
+ }
+
+ private int ownSampleRuns() {
+ return Arrays.stream(samples).sum();
+ }
+
+ public boolean filtered() {
+ return Arrays.stream(samples).sum() < Profile.FILTER;
+ }
+
+ public void add(State state, int samples, Method calling, boolean outsideCycle) {
+ if(outsideCycle)
+ this.samples[state.ordinal()] += samples;
+
+ if (calling != null)
+ calls.compute(calling, (pre, sample) -> sample == null ? samples : sample + samples);
+ }
+
+ private String percentage(int value) {
+ return df.format(value * 100.0 / ownSampleRuns());
+ }
+
+ private String totalPercentage(int value) {
+ return df.format(value * 100.0 / profile.getSampleRuns());
+ }
+
+ private String time(int value) {
+ return df.format(value * Profile.SAMPLING_SPEED / 1e9);
+ }
+
+ public void prune() {
+ calls.keySet().removeIf(Method::filtered);
+ calls.values().removeIf(s -> s < Profile.FILTER);
+ }
+
+ public void toDot(OutputStreamWriter writer) throws IOException {
+ int waiting = samples[State.WAITING.ordinal()];
+ int runnable = samples[State.RUNNING.ordinal()];
+ int io = samples[State.IO.ordinal()];
+ int time = samples[State.TIME.ordinal()];
+ int total = waiting + runnable + io + time;
+
+ int r = ( 255 * waiting + 192 * time) / total;
+ int g = (255 * runnable + 64 * waiting + 192 * io + 192 * time) / total;
+ int b = ( 64 * waiting + 255 * io + 192 * time) / total;
+ int a = (int) (255 * Math.sqrt(total / (double) profile.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(time(ownSampleRuns())).append("s ").append(totalPercentage(ownSampleRuns())).append("% R").append(percentage(runnable)).append("% T").append(percentage(time)).append("% I").append(percentage(io)).append("% W").append(percentage(waiting)).append("%\"];\n");
+
+ for (Map.Entry entry : calls.entrySet()) {
+ writer.append(String.valueOf(id)).append(" -> ").append(String.valueOf(entry.getKey().id)).append(" [label=\"").append(time(entry.getValue())).append("s\\n").append(percentage(entry.getValue())).append("%\",weight=").append(String.valueOf(entry.getValue())).append("];\n");
+ }
+ }
+
+ private String javaPrettify(Map mappings, String name) {
+ try {
+ int methodDivisor = name.lastIndexOf(".");
+ String className = name.substring(0, methodDivisor);
+ String methodDefinition = name.substring(methodDivisor + 1).replace('|', ';');
+ String methodName = methodDefinition.split("\\(", 2)[0];
+ String methodArgs = methodDefinition.split("\\(", 2)[1].split("\\)", 2)[0];
+ String methodReturn = methodDefinition.split("\\)", 2)[1];
+
+ ObfHelper.ClassMapping mapping = mappings.get(className);
+ if (mapping != null) {
+ className = mapping.mojangName;
+ methodName = mapping.methodsByObf.getOrDefault(methodDefinition, methodName);
+ }
+
+ if (className.contains("$$Lambda$")) {
+ className = className.split("\\$\\$Lambda\\$", 2)[0] + ".λ";
+ }
+
+ if (methodName.startsWith("lambda$")) {
+ methodName = methodName.split("\\$", 2)[1].replace('$', 'λ');
+ }
+
+ StringBuilder builder = new StringBuilder();
+ formatJvmTypes(mappings, builder, methodReturn);
+ builder.append("\\n").append(className.replace('$', '.')).append(".").append(methodName).append("\\n(");
+ formatJvmTypes(mappings, builder, methodArgs);
+ builder.append(")");
+
+ return builder.toString();
+ } catch (Throwable t) {
+ throw new IllegalArgumentException(t);
+ }
+ }
+
+ private void formatJvmTypes(Map mappings, StringBuilder builder, String jvmType) {
+ int i = 0;
+ while(i < jvmType.length()) {
+ int arrayDimensions = 0;
+ while(jvmType.charAt(i) == '[') {
+ arrayDimensions++;
+ i++;
+ }
+
+ switch (jvmType.charAt(i)) {
+ case 'B':
+ builder.append("byte");
+ i++;
+ break;
+ case 'C':
+ builder.append("char");
+ i++;
+ break;
+ case 'D':
+ builder.append("double");
+ i++;
+ break;
+ case 'F':
+ builder.append("float");
+ i++;
+ break;
+ case 'I':
+ builder.append("int");
+ i++;
+ break;
+ case 'J':
+ builder.append("long");
+ i++;
+ break;
+ case 'S':
+ builder.append("short");
+ i++;
+ break;
+ case 'Z':
+ builder.append("boolean");
+ i++;
+ break;
+ case 'V':
+ builder.append("void");
+ i++;
+ break;
+ case 'L':
+ int end = jvmType.indexOf(';', i++);
+ String className = jvmType.substring(i, end).replace('/', '.');
+ ObfHelper.ClassMapping mapping = mappings.get(className);
+ builder.append((mapping == null ? className : mapping.mojangName).replace('$', '.'));
+ i = end+1;
+ break;
+ default:
+ throw new IllegalArgumentException(jvmType + " at " + i);
+ }
+
+ while(arrayDimensions-- > 0) {
+ builder.append("[]");
+ }
+
+ if(i < jvmType.length())
+ builder.append(", ");
+ }
+ }
+}
diff --git a/src/main/java/de/steamwar/ObfHelper.java b/src/main/java/de/steamwar/ObfHelper.java
new file mode 100644
index 0000000..2590a26
--- /dev/null
+++ b/src/main/java/de/steamwar/ObfHelper.java
@@ -0,0 +1,81 @@
+package de.steamwar;
+
+import net.fabricmc.mappingio.MappingReader;
+import net.fabricmc.mappingio.format.MappingFormat;
+import net.fabricmc.mappingio.tree.MappingTree;
+import net.fabricmc.mappingio.tree.MemoryMappingTree;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class ObfHelper {
+ private ObfHelper() {}
+
+ public static final String MOJANG_PLUS_YARN_NAMESPACE = "mojang+yarn";
+ public static final String SPIGOT_NAMESPACE = "spigot";
+
+ public static Map loadMappingsIfPresent() {
+ for(Class> clazz : Agent.getInstrumentation().getAllLoadedClasses()) {
+ if(clazz.getName().equals("io.papermc.paper.util.ObfHelper")) {
+ try (final InputStream mappingsInputStream = clazz.getClassLoader().getResourceAsStream("META-INF/mappings/reobf.tiny")) {
+ if (mappingsInputStream == null)
+ return Collections.emptyMap();
+
+ final MemoryMappingTree tree = new MemoryMappingTree();
+ MappingReader.read(new InputStreamReader(mappingsInputStream, StandardCharsets.UTF_8), MappingFormat.TINY_2, tree);
+ Map classes = new HashMap<>();
+
+ for (final MappingTree.ClassMapping cls : tree.getClasses()) {
+ final Map methods = new HashMap<>();
+
+ for (final MappingTree.MethodMapping methodMapping : cls.getMethods()) {
+ methods.put(
+ methodKey(
+ methodMapping.getName(SPIGOT_NAMESPACE),
+ methodMapping.getDesc(SPIGOT_NAMESPACE)
+ ),
+ methodMapping.getName(MOJANG_PLUS_YARN_NAMESPACE)
+ );
+ }
+
+ final ClassMapping map = new ClassMapping(
+ cls.getName(SPIGOT_NAMESPACE).replace('/', '.'),
+ cls.getName(MOJANG_PLUS_YARN_NAMESPACE).replace('/', '.'),
+ methods
+ );
+ classes.put(map.obfName, map);
+ }
+
+ return classes;
+ } catch (final IOException ex) {
+ System.err.println("Failed to load mappings for stacktrace deobfuscation.");
+ ex.printStackTrace();
+ return Collections.emptyMap();
+ }
+ }
+ }
+
+ return Collections.emptyMap();
+ }
+
+ public static String methodKey(final String obfName, final String obfDescriptor) {
+ return obfName + obfDescriptor;
+ }
+
+ public static class ClassMapping {
+ public final String obfName;
+ public final String mojangName;
+ public final Map methodsByObf;
+
+ public ClassMapping(String obfName, String mojangName, Map methodsByObf) {
+ this.obfName = obfName;
+ this.mojangName = mojangName;
+ this.methodsByObf = methodsByObf;
+ }
+ }
+}
diff --git a/src/main/java/de/steamwar/Profile.java b/src/main/java/de/steamwar/Profile.java
new file mode 100644
index 0000000..9f8139f
--- /dev/null
+++ b/src/main/java/de/steamwar/Profile.java
@@ -0,0 +1,103 @@
+package de.steamwar;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.util.*;
+
+public class Profile {
+
+ public static final long SAMPLING_SPEED = 1000000;
+ public static final int FILTER = 100;
+
+ private static final HashMap stateMethods = new HashMap<>();
+ static {
+ stateMethods.put("jdk.internal.misc.Unsafe.park(ZJ)V", State.TIME);
+ stateMethods.put("java.lang.Thread.sleep(J)V", State.TIME);
+
+ stateMethods.put("java.lang.Object.wait(J)V", State.WAITING);
+ stateMethods.put("java.lang.Object.wait(JI)V", State.WAITING);
+ stateMethods.put("java.lang.ref.Reference.waitForReferencePendingList()V", State.WAITING);
+ stateMethods.put("com.ibm.lang.management.internal.MemoryNotificationThread.processNotificationLoop()V", State.WAITING);
+
+ stateMethods.put("sun.nio.ch.SocketDispatcher.read0(Ljava/io/FileDescriptor|JI)I", State.IO);
+ stateMethods.put("io.netty.channel.epoll.Native.epollWait(IJII)I", State.IO);
+ stateMethods.put("openj9.internal.tools.attach.target.IPC.waitSemaphore()I", State.IO);
+ stateMethods.put("java.io.FileInputStream.readBytes([BII)I", State.IO);
+ }
+
+ private static final HashSet omittedMethods = new HashSet<>();
+ static {
+ omittedMethods.add("java.lang.Thread.run()V");
+ omittedMethods.add("java.util.concurrent.FutureTask.run()V");
+ omittedMethods.add("java.util.concurrent.ForkJoinWorkerThread.run()V");
+ omittedMethods.add("java.util.concurrent.ForkJoinWorkerThread.runWorker(Ljava/util/concurrent/ForkJoinPool$WorkQueue|)V");
+ omittedMethods.add("java.util.concurrent.ThreadPoolExecutor$Worker.run()V");
+ omittedMethods.add("java.util.concurrent.ThreadPoolExecutor.runWorker(Ljava/util/concurrent/ThreadPoolExecutor$Worker|)V");
+ }
+
+ private final HashMap methods = new HashMap<>();
+ private final HashMap threads = new HashMap<>();
+
+ public Profile(Map mappings, String profile) {
+ HashSet previousIds = new HashSet<>();
+ for(String line : profile.split("\n")) {
+ previousIds.clear();
+
+ int sampleDivisor = line.lastIndexOf(" ");
+ int samples = Integer.parseInt(line.substring(sampleDivisor + 1));
+
+ String[] names = line.substring(0, sampleDivisor).split(";");
+ if(names.length <= 1)
+ continue;
+
+ State state = State.RUNNING;
+
+ for(String name : names) {
+ state = stateMethods.getOrDefault(name, state);
+ }
+
+ Method calling = null;
+ for(int i = names.length-1; i >= 0; i--) {
+ String name = names[i];
+
+ if(omittedMethods.contains(name))
+ continue;
+
+ Method method = methods.computeIfAbsent(name, id -> new Method(this, mappings, id));
+ method.add(state, samples, calling, previousIds.add(name));
+ calling = method;
+ }
+
+ threads.compute(calling, (thread, previous) -> previous == null ? samples : previous + samples);
+ }
+ }
+
+ public int getSampleRuns() {
+ return threads.values().stream().max(Integer::compareTo).orElse(0);
+ }
+
+ public void prune() {
+ methods.values().removeIf(Method::filtered);
+ methods.values().forEach(Method::prune);
+ }
+
+ public void save() {
+ prune();
+
+ File output = new File(System.getProperty("user.home"), "samples.dot");
+ try(OutputStreamWriter writer = new FileWriter(output)) {
+ //\ngraph [splines=true overlap=false];
+ writer.append("digraph {\ngraph [splines=true,overlap=false,searchsize=2];\nnode [shape=plaintext,style=\"filled\",margin=0.1];\n");
+
+ for(Method method : methods.values()) {
+ method.toDot(writer);
+ }
+
+ writer.append("}");
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/src/main/java/de/steamwar/Sampler.java b/src/main/java/de/steamwar/Sampler.java
deleted file mode 100644
index e05623a..0000000
--- a/src/main/java/de/steamwar/Sampler.java
+++ /dev/null
@@ -1,171 +0,0 @@
-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 int FILTER = 100;
-
- private static final Set waitingMethods = new HashSet<>();
- static {
- waitingMethods.add("io.netty.channel.epoll.Native.epollWait");
- waitingMethods.add("io.netty.channel.epoll.Native.epollWait0");
- waitingMethods.add("org.bukkit.craftbukkit.libs.jline.internal.NonBlockingInputStream.read");
- waitingMethods.add("net.minecrell.terminalconsole.SimpleTerminalConsole.readCommands");
- waitingMethods.add("com.ibm.lang.management.internal.MemoryNotificationThread.processNotificationLoop");
- waitingMethods.add("openj9.internal.tools.attach.target.IPC.waitSemaphore");
- waitingMethods.add("sun.nio.ch.Net.poll");
- waitingMethods.add("sun.nio.ch.SocketDispatcher.read0");
- waitingMethods.add("sun.awt.X11.XToolkit.waitForEvents");
- waitingMethods.add("java.lang.ProcessHandleImpl.waitForProcessExit0");
- waitingMethods.add("java.lang.ref.Reference.waitForReferencePendingList");
- waitingMethods.add("java.util.concurrent.locks.LockSupport.parkNanos");
- waitingMethods.add("java.io.FileInputStream.readBytes");
- }
-
- private static final Set omittedMethods = new HashSet<>();
- static {
- omittedMethods.add("java.lang.reflect.Method.invoke");
- omittedMethods.add("java.lang.Thread.run");
-
- omittedMethods.add("java.util.Iterator.forEachRemaining");
- omittedMethods.add("java.lang.Iterable.forEach");
- omittedMethods.add("java.util.ArrayList.forEach");
- omittedMethods.add("java.util.ArrayList$ArrayListSpliterator.forEachRemaining");
- omittedMethods.add("java.util.Map.forEach");
- omittedMethods.add("java.util.HashMap$EntrySpliterator.forEachRemaining");
- omittedMethods.add("java.util.Spliterators$IteratorSpliterator.forEachRemaining");
- omittedMethods.add("java.util.Spliterators$ArraySpliterator.forEachRemaining");
- omittedMethods.add("java.util.concurrent.ThreadPoolExecutor$Worker.run");
- omittedMethods.add("java.util.concurrent.ThreadPoolExecutor.runWorker");
- omittedMethods.add("java.util.concurrent.CompletableFuture$Completion.run");
- omittedMethods.add("java.util.concurrent.CompletableFuture$AsyncSupply.run");
- omittedMethods.add("java.util.stream.ReferencePipeline.forEach");
- omittedMethods.add("java.util.stream.ReferencePipeline.collect");
- omittedMethods.add("java.util.stream.ReferencePipeline$7$1.forEach");
- omittedMethods.add("java.util.stream.ReferencePipeline$3$1.accept");
- omittedMethods.add("java.util.stream.AbstractPipeline.evaluate");
- omittedMethods.add("java.util.stream.AbstractPipeline.wrapAndCopyInto");
- omittedMethods.add("java.util.stream.AbstractPipeline.copyInto");
- omittedMethods.add("java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential");
- omittedMethods.add("java.util.stream.ForEachOps$ForEachOp.evaluateSequential");
- omittedMethods.add("java.util.stream.ReduceOps$ReduceOp.evaluateSequential");
- omittedMethods.add("java.util.stream.ReduceOps$1ReducingSink.accept");
- omittedMethods.add("java.util.stream.Streams$StreamBuilderImpl.forEachRemaining");
- }
-
- private final Map traces = new HashMap<>();
- private final Thread samplerThread;
- private final Thread shutdownHook;
- private boolean shutdown = false;
-
- private int sampleRuns;
-
- public Sampler() {
- samplerThread = new Thread(this::run, "Sampler");
- samplerThread.setDaemon(true);
- samplerThread.start();
- shutdownHook = new Thread(this::stop, "SamplerShutdownHook");
- Runtime.getRuntime().addShutdownHook(shutdownHook);
- }
-
- public int getSampleRuns() {
- return sampleRuns;
- }
-
- public void stop() {
- shutdown = true;
- try {
- samplerThread.join();
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
-
- try {
- Runtime.getRuntime().removeShutdownHook(shutdownHook);
- } catch (IllegalStateException e) {
- //ignored
- }
-
- File output = new File(System.getProperty("user.home"), "samples.dot");
- try {
- OutputStreamWriter writer = new FileWriter(output);
- toDot(writer);
- writer.flush();
- writer.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
-
- private void run() {
- long lastTime = System.nanoTime();
- while(!shutdown) {
- sample();
-
- long currentTime = System.nanoTime();
- LockSupport.parkNanos(SAMPLING_SPEED - currentTime + lastTime);
- lastTime = currentTime;
- }
- }
-
- private void sample() {
- sampleRuns++;
-
- Map stackTraces = Thread.getAllStackTraces();
- Set previousIds = new HashSet<>();
-
- for(Map.Entry entry : stackTraces.entrySet()) {
- Thread thread = entry.getKey();
- StackTraceElement[] stack = entry.getValue();
- String threadName = thread.getName();
- if(stack.length == 0 || threadName.equals("Sampler"))
- continue;
-
- previousIds.clear();
-
- Thread.State state = thread.getState();
-
- String[] ids = new String[stack.length];
- for(int i = 0; i < stack.length; i++) {
- StackTraceElement ste = stack[i];
- String id = ste.getClassName() + "." + ste.getMethodName();
- ids[i] = id;
-
- if (waitingMethods.contains(id))
- state = Thread.State.WAITING;
- }
-
- Trace predecessor = traces.computeIfAbsent(threadName, name -> new Trace(this, threadName));
- predecessor.add(state, null);
-
- for(int i = stack.length - 1; i >= 0; i--) {
- String id = ids[i];
-
- if(!previousIds.add(id) || omittedMethods.contains(id))
- continue;
-
- Trace trace = traces.computeIfAbsent(id, id1 -> new Trace(this, id1));
- 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/State.java b/src/main/java/de/steamwar/State.java
new file mode 100644
index 0000000..3004ddc
--- /dev/null
+++ b/src/main/java/de/steamwar/State.java
@@ -0,0 +1,8 @@
+package de.steamwar;
+
+public enum State {
+ RUNNING,
+ WAITING,
+ TIME,
+ IO
+}
diff --git a/src/main/java/de/steamwar/Trace.java b/src/main/java/de/steamwar/Trace.java
deleted file mode 100644
index 527c39d..0000000
--- a/src/main/java/de/steamwar/Trace.java
+++ /dev/null
@@ -1,79 +0,0 @@
-package de.steamwar;
-
-import java.io.IOException;
-import java.io.OutputStreamWriter;
-import java.text.DecimalFormat;
-import java.util.Arrays;
-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 int[] samples = new int[6];
- private final Map predecessors = new HashMap<>();
-
- public Trace(Sampler sampler, String name) {
- this.sampler = sampler;
- id = idCounter++;
- this.name = name;
- }
-
- private int ownSampleRuns() {
- return Arrays.stream(samples).sum();
- }
-
- private boolean filtered() {
- return Arrays.stream(samples).sum() < Sampler.FILTER;
- }
-
- public void add(Thread.State state, Trace predecessor) {
- samples[state.ordinal()] += 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 / ownSampleRuns());
- }
-
- private String totalPercentage(int value) {
- return df.format(value * 100.0 / sampler.getSampleRuns());
- }
-
- private String time(int value) {
- return df.format(value * Sampler.SAMPLING_SPEED / 1e9);
- }
-
- public void toDot(OutputStreamWriter writer) throws IOException {
- if (filtered())
- return;
-
- int waiting = samples[3] + samples[4] + samples[5];
- int runnable = samples[1] + samples[0];
- int blocked = samples[2];
- 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(time(ownSampleRuns())).append("s ").append(totalPercentage(ownSampleRuns())).append("%\\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() < Sampler.FILTER)
- continue;
-
- writer.append(String.valueOf(entry.getKey().id)).append(" -> ").append(String.valueOf(id)).append(" [label=\"").append(time(entry.getValue())).append("s\\n").append(percentage(entry.getValue())).append("%\",weight=").append(String.valueOf(entry.getValue())).append("];\n");
- }
- }
-}