From 3651a3bab208dda1ada8f0cfd2d9cd09ea9f5b9c Mon Sep 17 00:00:00 2001 From: Lixfel Date: Mon, 28 Nov 2022 21:46:44 +0100 Subject: [PATCH 1/2] WIP (still occasional SIGSEGV on OpenJ9) --- .gitignore | 1 + pom.xml | 60 ++++++-- src/main/java/de/steamwar/Agent.java | 53 +++++-- src/main/java/de/steamwar/Method.java | 178 +++++++++++++++++++++++ src/main/java/de/steamwar/ObfHelper.java | 81 +++++++++++ src/main/java/de/steamwar/Profile.java | 104 +++++++++++++ src/main/java/de/steamwar/Sampler.java | 171 ---------------------- src/main/java/de/steamwar/State.java | 7 + src/main/java/de/steamwar/Trace.java | 79 ---------- 9 files changed, 459 insertions(+), 275 deletions(-) create mode 100644 src/main/java/de/steamwar/Method.java create mode 100644 src/main/java/de/steamwar/ObfHelper.java create mode 100644 src/main/java/de/steamwar/Profile.java delete mode 100644 src/main/java/de/steamwar/Sampler.java create mode 100644 src/main/java/de/steamwar/State.java delete mode 100644 src/main/java/de/steamwar/Trace.java 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..8a8b96e 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 + + + false + + + + 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..88e7a35 100644 --- a/src/main/java/de/steamwar/Agent.java +++ b/src/main/java/de/steamwar/Agent.java @@ -1,32 +1,67 @@ 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; 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,alluser"); // 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..543700d --- /dev/null +++ b/src/main/java/de/steamwar/Method.java @@ -0,0 +1,178 @@ +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 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[3]; + private final HashMap calls = new HashMap<>(); + + public Method(Profile profile, Map mappings, String name) { + this.profile = profile; + this.id = idCounter++; + + int methodDivisor = name.lastIndexOf("."); + if(methodDivisor != -1) { + 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(" ").append(className.replace('$', '.')).append(".").append(methodName).append("("); + formatJvmTypes(mappings, builder, methodArgs); + builder.append(")"); + + name = builder.toString(); + } + this.name = name; + } + + private int ownSampleRuns() { + return Arrays.stream(samples).sum(); + } + + private 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 toDot(OutputStreamWriter writer) throws IOException { + if (filtered()) + return; + + int waiting = samples[State.WAITING.ordinal()]; + int runnable = samples[State.RUNNING.ordinal()]; + int blocked = samples[State.BLOCKED.ordinal()]; + 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) 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("%\\nR").append(percentage(runnable)).append("% B").append(percentage(blocked)).append("% W").append(percentage(waiting)).append("%\"];\n"); + + for (Map.Entry entry : calls.entrySet()) { + if (entry.getKey().filtered() || entry.getValue() < Profile.FILTER) + continue; + + 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 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..a79a9ac --- /dev/null +++ b/src/main/java/de/steamwar/Profile.java @@ -0,0 +1,104 @@ +package de.steamwar; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +public class Profile { + + public static final long SAMPLING_SPEED = 1000000; + public static final int FILTER = 100; + + private static final HashSet waitingMethods = new HashSet<>(); + static { + waitingMethods.add("jdk.internal.misc.Unsafe.park(ZJ)V"); + waitingMethods.add("java.lang.Thread.sleep(J)V"); + waitingMethods.add("java.lang.Object.wait(J)V"); + waitingMethods.add("java.lang.Object.wait(JI)V"); + waitingMethods.add("java.lang.ref.Reference.waitForReferencePendingList()V"); + waitingMethods.add("accept"); + waitingMethods.add("poll"); + waitingMethods.add("com.ibm.lang.management.internal.MemoryNotificationThread.processNotificationLoop()V"); + waitingMethods.add("openj9.internal.tools.attach.target.IPC.waitSemaphore()I"); + waitingMethods.add("net.minecrell.terminalconsole.SimpleTerminalConsole.readCommands(Ljava/io/InputStream|)V"); + waitingMethods.add("sun.nio.ch.SocketDispatcher.read0(Ljava/io/FileDescriptor|JI)I"); + waitingMethods.add("io.netty.channel.epoll.Native.epollWait(IJII)I"); + } + //private static final HashSet blockingMethods = new HashSet<>(); For later if possible + private static final HashSet omittedMethods = new HashSet<>(); + static { + omittedMethods.add("/usr/lib/libc.so.6"); + omittedMethods.add("java.lang.Thread.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 threadSamples = 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) { + if (waitingMethods.contains(name)) { + state = State.WAITING; + break; + } + } + + 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; + } + + threadSamples.compute(names[0], (threadName, previous) -> previous == null ? samples : previous + samples); + } + } + + public int getSampleRuns() { + return threadSamples.values().stream().max(Integer::compareTo).orElse(0); + } + + public void save() { + File output = new File(System.getProperty("user.home"), "samples.dot"); + try(OutputStreamWriter writer = new FileWriter(output)) { + + writer.append("digraph {\nnode [shape=plaintext,style=\"filled\", margin=0.1];\n"); + + for(Method method : methods.values()) { + + method.toDot(writer); + } + + writer.append("}"); + + //writer.flush(); + } 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..e915a00 --- /dev/null +++ b/src/main/java/de/steamwar/State.java @@ -0,0 +1,7 @@ +package de.steamwar; + +public enum State { + RUNNING, + WAITING, + BLOCKED +} 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"); - } - } -} From 25afe9f60581b82dea1593deb43ea32bfa361f68 Mon Sep 17 00:00:00 2001 From: Lixfel Date: Tue, 29 Nov 2022 11:49:57 +0100 Subject: [PATCH 2/2] Current state --- pom.xml | 2 +- src/main/java/de/steamwar/Agent.java | 3 +- src/main/java/de/steamwar/Method.java | 101 ++++++++++++++----------- src/main/java/de/steamwar/Profile.java | 59 +++++++-------- src/main/java/de/steamwar/State.java | 3 +- 5 files changed, 90 insertions(+), 78 deletions(-) diff --git a/pom.xml b/pom.xml index 8a8b96e..cca24b1 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ shade - false + true diff --git a/src/main/java/de/steamwar/Agent.java b/src/main/java/de/steamwar/Agent.java index 88e7a35..ad8324d 100644 --- a/src/main/java/de/steamwar/Agent.java +++ b/src/main/java/de/steamwar/Agent.java @@ -20,6 +20,7 @@ public class Agent { public static void agentmain(String args, Instrumentation inst) { instrumentation = inst; + //AsyncProfiler.getInstance("/home/lixfel/libasyncProfiler.so"); switch (args) { case "start": start(); @@ -37,7 +38,7 @@ public class Agent { if(shutdownHook != null) return; - callProfiler("start,event=wall,threads,interval=1000000,alluser"); // 1mio ns = 1ms + callProfiler("start,event=wall,threads,interval=1000000,cstack=fp"); // 1mio ns = 1ms shutdownHook = new Thread(Agent::stop, "SamplerShutdownHook"); Runtime.getRuntime().addShutdownHook(shutdownHook); } diff --git a/src/main/java/de/steamwar/Method.java b/src/main/java/de/steamwar/Method.java index 543700d..1572d80 100644 --- a/src/main/java/de/steamwar/Method.java +++ b/src/main/java/de/steamwar/Method.java @@ -3,9 +3,7 @@ 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; +import java.util.*; public class Method { private static final DecimalFormat df = new DecimalFormat("0.00"); @@ -16,43 +14,21 @@ public class Method { private final int id; private final String name; - private final int[] samples = new int[3]; + 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++; - int methodDivisor = name.lastIndexOf("."); - if(methodDivisor != -1) { - 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); + try { + name = javaPrettify(mappings, name); + } catch (IllegalArgumentException e) { + if(name.startsWith("/")) { // native lib + name = name.substring(name.lastIndexOf('/') + 1); } - - 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(" ").append(className.replace('$', '.')).append(".").append(methodName).append("("); - formatJvmTypes(mappings, builder, methodArgs); - builder.append(")"); - - name = builder.toString(); } + this.name = name; } @@ -60,7 +36,7 @@ public class Method { return Arrays.stream(samples).sum(); } - private boolean filtered() { + public boolean filtered() { return Arrays.stream(samples).sum() < Profile.FILTER; } @@ -84,32 +60,67 @@ public class Method { return df.format(value * Profile.SAMPLING_SPEED / 1e9); } - public void toDot(OutputStreamWriter writer) throws IOException { - if (filtered()) - return; + 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 blocked = samples[State.BLOCKED.ordinal()]; - int total = waiting + runnable + blocked; + int io = samples[State.IO.ordinal()]; + int time = samples[State.TIME.ordinal()]; + int total = waiting + runnable + io + time; - int r = (255 * blocked + 192 * waiting) / total; - int g = (255 * runnable + 192 * waiting) / total; - int b = (192 * waiting) / total; + 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("%\\nR").append(percentage(runnable)).append("% B").append(percentage(blocked)).append("% W").append(percentage(waiting)).append("%\"];\n"); + 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()) { - if (entry.getKey().filtered() || entry.getValue() < Profile.FILTER) - continue; - 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()) { diff --git a/src/main/java/de/steamwar/Profile.java b/src/main/java/de/steamwar/Profile.java index a79a9ac..9f8139f 100644 --- a/src/main/java/de/steamwar/Profile.java +++ b/src/main/java/de/steamwar/Profile.java @@ -4,35 +4,33 @@ import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.OutputStreamWriter; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; +import java.util.*; public class Profile { public static final long SAMPLING_SPEED = 1000000; public static final int FILTER = 100; - private static final HashSet waitingMethods = new HashSet<>(); + private static final HashMap stateMethods = new HashMap<>(); static { - waitingMethods.add("jdk.internal.misc.Unsafe.park(ZJ)V"); - waitingMethods.add("java.lang.Thread.sleep(J)V"); - waitingMethods.add("java.lang.Object.wait(J)V"); - waitingMethods.add("java.lang.Object.wait(JI)V"); - waitingMethods.add("java.lang.ref.Reference.waitForReferencePendingList()V"); - waitingMethods.add("accept"); - waitingMethods.add("poll"); - waitingMethods.add("com.ibm.lang.management.internal.MemoryNotificationThread.processNotificationLoop()V"); - waitingMethods.add("openj9.internal.tools.attach.target.IPC.waitSemaphore()I"); - waitingMethods.add("net.minecrell.terminalconsole.SimpleTerminalConsole.readCommands(Ljava/io/InputStream|)V"); - waitingMethods.add("sun.nio.ch.SocketDispatcher.read0(Ljava/io/FileDescriptor|JI)I"); - waitingMethods.add("io.netty.channel.epoll.Native.epollWait(IJII)I"); + 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 blockingMethods = new HashSet<>(); For later if possible + private static final HashSet omittedMethods = new HashSet<>(); static { - omittedMethods.add("/usr/lib/libc.so.6"); 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"); @@ -40,7 +38,7 @@ public class Profile { } private final HashMap methods = new HashMap<>(); - private final HashMap threadSamples = new HashMap<>(); + private final HashMap threads = new HashMap<>(); public Profile(Map mappings, String profile) { HashSet previousIds = new HashSet<>(); @@ -57,10 +55,7 @@ public class Profile { State state = State.RUNNING; for(String name : names) { - if (waitingMethods.contains(name)) { - state = State.WAITING; - break; - } + state = stateMethods.getOrDefault(name, state); } Method calling = null; @@ -75,28 +70,32 @@ public class Profile { calling = method; } - threadSamples.compute(names[0], (threadName, previous) -> previous == null ? samples : previous + samples); + threads.compute(calling, (thread, previous) -> previous == null ? samples : previous + samples); } } public int getSampleRuns() { - return threadSamples.values().stream().max(Integer::compareTo).orElse(0); + 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)) { - - writer.append("digraph {\nnode [shape=plaintext,style=\"filled\", margin=0.1];\n"); + //\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("}"); - - //writer.flush(); } catch (IOException e) { e.printStackTrace(); } diff --git a/src/main/java/de/steamwar/State.java b/src/main/java/de/steamwar/State.java index e915a00..3004ddc 100644 --- a/src/main/java/de/steamwar/State.java +++ b/src/main/java/de/steamwar/State.java @@ -3,5 +3,6 @@ package de.steamwar; public enum State { RUNNING, WAITING, - BLOCKED + TIME, + IO }