From f8a11aec7468be71b746d00a7b0f438c95b4419f Mon Sep 17 00:00:00 2001 From: "Egor.Ushakov" Date: Tue, 26 Sep 2017 11:48:44 +0300 Subject: [PATCH] debugger agent: implemented recursive async handling --- .../rt/debugger/agent/CaptureStorage.java | 116 +++++++++++-- .../rt/debugger/agent/CaptureAgent.java | 154 ++++++++++++++++-- .../StackCapturingLineBreakpoint.java | 18 +- 3 files changed, 257 insertions(+), 31 deletions(-) diff --git a/java/debugger/debugger-agent-storage/src/com/intellij/rt/debugger/agent/CaptureStorage.java b/java/debugger/debugger-agent-storage/src/com/intellij/rt/debugger/agent/CaptureStorage.java index ade32caec0a6..5ee7e7f3c56c 100644 --- a/java/debugger/debugger-agent-storage/src/com/intellij/rt/debugger/agent/CaptureStorage.java +++ b/java/debugger/debugger-agent-storage/src/com/intellij/rt/debugger/agent/CaptureStorage.java @@ -15,9 +15,10 @@ */ package com.intellij.rt.debugger.agent; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; +import sun.misc.JavaLangAccess; +import sun.misc.SharedSecrets; + +import java.util.*; /** * @author egor @@ -25,22 +26,117 @@ import java.util.Map; public class CaptureStorage { private static final int MAX_STORED_STACKS = 1000; - public static final Map STORAGE = Collections.synchronizedMap(new LinkedHashMap() { + private static final Map STORAGE = Collections.synchronizedMap(new LinkedHashMap() { @Override protected boolean removeEldestEntry(Map.Entry eldest) { return size() > MAX_STORED_STACKS; } }); + private static final ThreadLocal> CURRENT_STACKS = ThreadLocal.withInitial(LinkedList::new); + private static final JavaLangAccess ourJavaLangAccess = SharedSecrets.getJavaLangAccess(); + + @SuppressWarnings("unused") + public static void capture(Object key) { + System.out.println("capture: " + key); + Deque currentStacks = CURRENT_STACKS.get(); + CapturedStack stack; + if (currentStacks.isEmpty()) { + stack = new CapturedStack(new Throwable()); + } + else { + stack = new CapturedStack(new Throwable(), currentStacks.getLast()); + System.out.println("capture: insertion added"); + } + STORAGE.put(key, stack); + } + + @SuppressWarnings("unused") + public static void insertEnter(Object key) { + System.out.println("insert ->: " + key); + CapturedStack stack = STORAGE.get(key); + Deque currentStacks = CURRENT_STACKS.get(); + if (stack != null) { + currentStacks.add(new InsertMatch(stack, ourJavaLangAccess.getStackTraceDepth(new Throwable()))); + System.out.println("insert ->: stack saved (" + currentStacks.size() + ")"); + } + else { + currentStacks.add(InsertMatch.EMPTY); + System.out.println("insert ->: no stack found (" + currentStacks.size() + ")"); + } + } + + @SuppressWarnings("unused") + public static void insertExit(Object key) { + System.out.println("insert <-: " + key); + Deque currentStacks = CURRENT_STACKS.get(); + if (currentStacks.isEmpty()) { + System.out.println("insert error, empty stack: " + key); + return; + } + currentStacks.removeLast(); + System.out.println("insert <-: stack removed (" + currentStacks.size() + ")"); + } + + // TODO: may save space using a separate CapturedStack w/o replace stack when can + private static class CapturedStack { + private final Throwable myException; + private final InsertMatch myInsertMatch; + + public CapturedStack(Throwable exception) { + this(exception, InsertMatch.EMPTY); + } + + public CapturedStack(Throwable exception, InsertMatch insertMatch) { + myException = exception; + myInsertMatch = insertMatch; + } + + List getStackTrace() { + StackTraceElement[] stackTrace = myException.getStackTrace(); + if (myInsertMatch == null || myInsertMatch == InsertMatch.EMPTY) { + return Arrays.asList(stackTrace).subList(1, stackTrace.length); + } + else { + List insertStack = myInsertMatch.myStack.getStackTrace(); + int insertPos = stackTrace.length - myInsertMatch.myDepth + 2; + ArrayList res = new ArrayList<>(insertPos + insertStack.size() + 1); + res.addAll(Arrays.asList(stackTrace).subList(1, insertPos)); + res.add(null); + res.addAll(insertStack); + return res; + } + } + } + + private static class InsertMatch { + private final CapturedStack myStack; + private final int myDepth; + + static final InsertMatch EMPTY = new InsertMatch(null, 0); + + private InsertMatch(CapturedStack stack, int depth) { + myStack = stack; + myDepth = depth; + } + } // to be run from the debugger @SuppressWarnings("unused") public static Object[][] getRelatedStack(Object key) { - Exception exception = STORAGE.get(key); - StackTraceElement[] stackTrace = exception.getStackTrace(); - Object[][] res = new Object[stackTrace.length][]; - for (int i = 0; i < stackTrace.length; i++) { - StackTraceElement elem = stackTrace[i]; - res[i] = new Object[]{elem.getClassName(), elem.getFileName(), elem.getMethodName(), String.valueOf(elem.getLineNumber())}; + CapturedStack stack = STORAGE.get(key); + if (stack == null) { + return null; + } + List stackTrace = stack.getStackTrace(); + Object[][] res = new Object[stackTrace.size()][]; + for (int i = 0; i < stackTrace.size(); i++) { + StackTraceElement elem = stackTrace.get(i); + if (elem == null) { + res[i] = null; + } + else { + res[i] = new Object[]{elem.getClassName(), elem.getFileName(), elem.getMethodName(), String.valueOf(elem.getLineNumber())}; + } } return res; } diff --git a/java/debugger/debugger-agent/src/com/intellij/rt/debugger/agent/CaptureAgent.java b/java/debugger/debugger-agent/src/com/intellij/rt/debugger/agent/CaptureAgent.java index b12cdc214ed3..d533f8eed3f4 100644 --- a/java/debugger/debugger-agent/src/com/intellij/rt/debugger/agent/CaptureAgent.java +++ b/java/debugger/debugger-agent/src/com/intellij/rt/debugger/agent/CaptureAgent.java @@ -23,9 +23,11 @@ import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.Instrumentation; import java.lang.instrument.UnmodifiableClassException; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.security.ProtectionDomain; import java.util.*; +import java.util.function.Supplier; import java.util.jar.JarFile; /** @@ -33,12 +35,20 @@ import java.util.jar.JarFile; */ public class CaptureAgent { private static Instrumentation ourInstrumentation; + private static volatile Map> myCapturePoints = new HashMap<>(); static { CapturePoint invokeLater = new CapturePoint("javax/swing/SwingUtilities", "invokeLater", 0); myCapturePoints.put(invokeLater.myClassName, Collections.singletonList(invokeLater)); } + private static volatile Map> myInsertPoints = new HashMap<>(); + + static { + InsertPoint invokeLater = new InsertPoint("java/awt/event/InvocationEvent", "dispatch", "runnable", "Ljava/lang/Runnable;"); + myInsertPoints.put(invokeLater.myClassName, Collections.singletonList(invokeLater)); + } + public static void premain(String args, Instrumentation instrumentation) throws IOException { ourInstrumentation = instrumentation; instrumentation.appendToBootstrapClassLoaderSearch(createTempJar("debugger-agent-storage.jar")); @@ -47,6 +57,10 @@ public class CaptureAgent { System.out.println("Capture agent: ready"); } + private static List getNotNull(List list) { + return list != null ? list : Collections.emptyList(); + } + private static class CaptureTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, @@ -54,14 +68,36 @@ public class CaptureAgent { Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { - List capturePoints = myCapturePoints.get(className); - if (!capturePoints.isEmpty()) { + List capturePoints = getNotNull(myCapturePoints.get(className)); + List insertPoints = getNotNull(myInsertPoints.get(className)); + if (!capturePoints.isEmpty() || !insertPoints.isEmpty()) { ClassReader reader = new ClassReader(classfileBuffer); - ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS); + ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES); for (CapturePoint point : capturePoints) { - reader.accept(new CaptureInstrumentor(Opcodes.ASM6, writer, point), 0); + try { + reader.accept(new CaptureInstrumentor(Opcodes.ASM6, writer, point), 0); + } catch (Exception e) { + e.printStackTrace(); + } } - return writer.toByteArray(); + for (InsertPoint point : insertPoints) { + try { + reader.accept(new InsertInstrumentor(Opcodes.ASM6, writer, point), 0); + } catch (Exception e) { + e.printStackTrace(); + } + } + byte[] bytes = writer.toByteArray(); + + try { + Path path = new File("instrumented_" + className.replaceAll("/", "_") + ".class").toPath(); + Files.write(path, bytes); + } + catch (IOException e) { + e.printStackTrace(); + } + + return bytes; } return null; } @@ -79,18 +115,13 @@ public class CaptureAgent { public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); if (capturePoint.myMethodName.equals(name)) { - System.out.println("Capture agent: instrumented " + capturePoint.myClassName + "." + name); + System.out.println("Capture agent: instrumented capture point at " + capturePoint.myClassName + "." + name); return new MethodVisitor(api, mv) { @Override public void visitCode() { - visitFieldInsn(Opcodes.GETSTATIC, CaptureStorage.class.getName().replaceAll("\\.", "/"), "STORAGE", "Ljava/util/Map;"); visitVarInsn(Opcodes.ALOAD, capturePoint.myParamSlotId); - visitTypeInsn(Opcodes.NEW, "java/lang/Exception"); - visitInsn(Opcodes.DUP); - visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Exception", "", "()V", false); - visitMethodInsn(Opcodes.INVOKEINTERFACE, "java/util/Map", "put", - "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", true); - visitInsn(Opcodes.POP); + visitMethodInsn(Opcodes.INVOKESTATIC, CaptureStorage.class.getName().replaceAll("\\.", "/"), "capture", + "(Ljava/lang/Object;)V", false); super.visitCode(); } }; @@ -99,15 +130,104 @@ public class CaptureAgent { } } + private static class InsertInstrumentor extends ClassVisitor { + private InsertPoint myInsertPoint; + Supplier myVisitMethod = null; + String myDesc; + + public InsertInstrumentor(int api, ClassVisitor cv, InsertPoint insertPoint) { + super(api, cv); + this.myInsertPoint = insertPoint; + } + + private static String getNewName(String name) { + return name + "$$$capture"; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + if (myInsertPoint.myMethodName.equals(name)) { + MethodVisitor mv = super.visitMethod(access, getNewName(name), desc, signature, exceptions); + myDesc = desc; + myVisitMethod = () -> super.visitMethod(access, name, desc, signature, exceptions); + System.out.println("Capture agent: instrumented insert point at " + myInsertPoint.myClassName + "." + name); + return mv; + } + return super.visitMethod(access, name, desc, signature, exceptions); + } + + @Override + public void visitEnd() { + if (myVisitMethod != null) { + MethodVisitor mv = myVisitMethod.get(); + + Label start = new Label(); + mv.visitLabel(start); + + insertEnter(mv); + + mv.visitVarInsn(Opcodes.ALOAD, 0); + // TODO: mv.loadArgs(); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, myInsertPoint.myClassName, getNewName(myInsertPoint.myMethodName), myDesc, false); + + Label end = new Label(); + mv.visitLabel(end); + + // regular exit + insertExit(mv); + mv.visitInsn(Opcodes.RETURN); + + Label catchLabel = new Label(); + mv.visitLabel(catchLabel); + mv.visitTryCatchBlock(start, end, catchLabel, null); + + // exception exit + insertExit(mv); + mv.visitInsn(Opcodes.ATHROW); + + mv.visitMaxs(0, 0); + mv.visitEnd(); + } + } + + private void insertEnter(MethodVisitor mv) { + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitFieldInsn(Opcodes.GETFIELD, myInsertPoint.myClassName, myInsertPoint.myField, myInsertPoint.myFieldDesc); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, CaptureStorage.class.getName().replaceAll("\\.", "/"), "insertEnter", + "(Ljava/lang/Object;)V", false); + } + + private void insertExit(MethodVisitor mv) { + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitFieldInsn(Opcodes.GETFIELD, myInsertPoint.myClassName, myInsertPoint.myField, myInsertPoint.myFieldDesc); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, CaptureStorage.class.getName().replaceAll("\\.", "/"), "insertExit", + "(Ljava/lang/Object;)V", false); + } + } + static class CapturePoint { final String myClassName; final String myMethodName; final int myParamSlotId; - public CapturePoint(String myClassName, String myMethodName, int myParamSlotId) { - this.myClassName = myClassName; - this.myMethodName = myMethodName; - this.myParamSlotId = myParamSlotId; + public CapturePoint(String className, String methodName, int paramSlotId) { + this.myClassName = className; + this.myMethodName = methodName; + this.myParamSlotId = paramSlotId; + } + } + + static class InsertPoint { + final String myClassName; + final String myMethodName; + final String myField; + final String myFieldDesc; + + public InsertPoint(String className, String methodName, String field, String fieldDesc) { + this.myClassName = className; + this.myMethodName = methodName; + this.myField = field; + myFieldDesc = fieldDesc; } } diff --git a/java/debugger/impl/src/com/intellij/debugger/ui/breakpoints/StackCapturingLineBreakpoint.java b/java/debugger/impl/src/com/intellij/debugger/ui/breakpoints/StackCapturingLineBreakpoint.java index 64fc9b0e6ff7..05685fc49266 100644 --- a/java/debugger/impl/src/com/intellij/debugger/ui/breakpoints/StackCapturingLineBreakpoint.java +++ b/java/debugger/impl/src/com/intellij/debugger/ui/breakpoints/StackCapturingLineBreakpoint.java @@ -303,10 +303,15 @@ public class StackCapturingLineBreakpoint extends WildcardMethodBreakpoint { List values = ((ArrayReference)resArray).getValues(); List res = new ArrayList<>(values.size()); for (Value value : values) { - List values1 = ((ArrayReference)value).getValues(); - res.add(new ProcessStackFrameItem(getStringRefValue((StringReference)values1.get(0)), - getStringRefValue((StringReference)values1.get(2)), - Integer.parseInt(((StringReference)values1.get(3)).value()))); + if (value == null) { + res.add(null); + } + else { + List values1 = ((ArrayReference)value).getValues(); + res.add(new ProcessStackFrameItem(getStringRefValue((StringReference)values1.get(0)), + getStringRefValue((StringReference)values1.get(2)), + Integer.parseInt(((StringReference)values1.get(3)).value()))); + } } return res; } @@ -346,6 +351,11 @@ public class StackCapturingLineBreakpoint extends WildcardMethodBreakpoint { public String method() { return myMethod; } + + @Override + public String toString() { + return myClass + "." + myMethod + ":" + myLine; + } } @Nullable