From 230209436d3ea4f55de79e7d69e4553770397e2c Mon Sep 17 00:00:00 2001 From: Lixfel Date: Sun, 5 Dec 2021 21:21:05 +0100 Subject: [PATCH] Profiler 1.0 --- pom.xml | 13 ++- src/main/java/de/steamwar/Agent.java | 29 +++++ src/main/java/de/steamwar/AttachTool.java | 39 ------- src/main/java/de/steamwar/Main.java | 51 ++++++++ src/main/java/de/steamwar/Sampler.java | 134 ++++++++++++++++++++++ src/main/java/de/steamwar/Trace.java | 81 +++++++++++++ 6 files changed, 304 insertions(+), 43 deletions(-) create mode 100644 src/main/java/de/steamwar/Agent.java delete mode 100644 src/main/java/de/steamwar/AttachTool.java create mode 100644 src/main/java/de/steamwar/Main.java create mode 100644 src/main/java/de/steamwar/Sampler.java create mode 100644 src/main/java/de/steamwar/Trace.java 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"); + } + } +}