From ffc4316cfe5cf5c1749c3cf23a8cc1faaff28290 Mon Sep 17 00:00:00 2001 From: "Alexey.Merkulov" Date: Thu, 3 Jul 2025 15:54:36 +0200 Subject: [PATCH 1/8] Implement an initial version of instrumentation conditional breakpoints --- .../ConditionalBreakpointTransformer.java | 230 ++++++++++++++++++ .../rt/debugger/agent/DebuggerAgent.java | 1 + 2 files changed, 231 insertions(+) create mode 100644 src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java diff --git a/src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java b/src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java new file mode 100644 index 0000000..2876ef4 --- /dev/null +++ b/src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java @@ -0,0 +1,230 @@ +package com.intellij.rt.debugger.agent; + +import org.jetbrains.capture.org.objectweb.asm.*; +import org.jetbrains.capture.org.objectweb.asm.tree.ClassNode; +import org.jetbrains.capture.org.objectweb.asm.tree.LocalVariableNode; +import org.jetbrains.capture.org.objectweb.asm.tree.MethodNode; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.Instrumentation; +import java.security.ProtectionDomain; +import java.util.*; + +import static com.intellij.rt.debugger.agent.CaptureAgent.getInternalClsName; + +class InstrumentationBpExceptionWrapper extends RuntimeException { + final int instrumentationId; + + InstrumentationBpExceptionWrapper(Throwable e, int instrumentationId) { + super(e); + this.instrumentationId = instrumentationId; + } +} + +class InstrumentationBreakpointInfo { + final Integer lineNumber; + final String fragmentClassName; + final String methodSignature; + final List argumentNames; + + InstrumentationBreakpointInfo(Integer lineNumber, String fragmentClassName, String methodSignature, List argumentNames) { + this.lineNumber = lineNumber; + this.fragmentClassName = fragmentClassName; + this.methodSignature = methodSignature; + this.argumentNames = argumentNames; + } +} + +public class ConditionalBreakpointTransformer { + private static final String conditionCheckMethodName = "conditionCheck"; + + private static final Map>> myBreakpoints = new LinkedHashMap<>(); + + public static void init(Properties properties, Instrumentation instrumentation) { + applyProperties(properties); + instrumentation.addTransformer(new BreakpointInstrumentalist(), true); + } + + private static void applyProperties(Properties properties) { + for (Map.Entry entry : properties.entrySet()) { + addPoint((String)entry.getKey(), (String)entry.getValue()); + } + } + + private static void addPoint(String propertyKey, String propertyValue) { + if (propertyKey.startsWith("instrumentation.condition.breakpoint")) { + String[] split = propertyValue.split(" "); + if (split.length >= 4) { + addBreakpoint(Arrays.asList(split)); + } + } + } + + public static void addBreakpoint(List split) { + String className = split.get(0); + String methodName = split.get(1); + int lineNumber = Integer.parseInt(split.get(2)); + String fragmentClassName = split.get(3); + String methodSignature = split.get(4); + List argumentNames = split.subList(5, split.size()); + + Map> methods = myBreakpoints.get(className); + if (methods == null) { + methods = new LinkedHashMap<>(); + myBreakpoints.put(className, methods); + } + Map lineNumbers = getLineNumbers(methodName, methods); + if (lineNumbers == null) { + lineNumbers = new LinkedHashMap<>(); + methods.put(methodName, lineNumbers); + } + lineNumbers.put(lineNumber, new InstrumentationBreakpointInfo(lineNumber, fragmentClassName, methodSignature, argumentNames)); + } + + private static class BreakpointInstrumentalist implements ClassFileTransformer { + @Override + public byte[] transform(final ClassLoader loader, final String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { + final Map> methods = myBreakpoints.get(className); + if (methods == null || methods.isEmpty()) { + return null; + } + + try { + final ClassNode classNode = new ClassNode(); + new ClassReader(classfileBuffer).accept(classNode, 0); + + ClassTransformer transformer = new ClassTransformer( + className, classfileBuffer, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS, loader + ); + + return transformer.accept(new ClassVisitor(Opcodes.API_VERSION, transformer.writer) { + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + MethodVisitor superMethodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions); + final Map lineNumbers = getLineNumbers(name, methods); + if (lineNumbers == null || lineNumbers.isEmpty()) { + return superMethodVisitor; + } + + MethodNode m = null; + for (MethodNode method : classNode.methods) { + if (method.name.equals(name) && method.desc.equals(descriptor)) { + m = method; + } + } + if (m == null) { + return superMethodVisitor; + } + final MethodNode methodNode = m; + + return new MethodVisitor(api, superMethodVisitor) { + + @Override + public void visitLineNumber(int line, Label start) { + InstrumentationBreakpointInfo instrumentationBreakpointInfo = lineNumbers.get(line); + if (instrumentationBreakpointInfo != null) { + addInstrumentationCondition(instrumentationBreakpointInfo, start); + } + super.visitLineNumber(line, start); + } + + private void addInstrumentationCondition(InstrumentationBreakpointInfo instrumentationBreakpointInfo, Label start) { + String fragmentClassName = instrumentationBreakpointInfo.fragmentClassName; + int instrumentationId = extractIdFromFragmentClassName(fragmentClassName); + try { + Label startTry = new Label(); + Label endTry = new Label(); + Label catchBlock = new Label(); + Label afterIf = new Label(); + + mv.visitTryCatchBlock(startTry, endTry, catchBlock, "java/lang/Throwable"); + + mv.visitLabel(startTry); + + for (String argumentName : instrumentationBreakpointInfo.argumentNames) { + if (argumentName.equals("this")/* || argumentName.startsWith("this$")*/) { + mv.visitVarInsn(Opcodes.ALOAD, 0); + } + else { + for (LocalVariableNode localVariable : methodNode.localVariables) { + if (localVariable.name.equals(argumentName) /*&& + start.getOffset() >= localVariable.start.getLabel().getOffset() && + start.getOffset() < localVariable.end.getLabel().getOffset()*/) { + Type type = Type.getType(localVariable.desc); + mv.visitVarInsn(type.getOpcode(Opcodes.ILOAD), localVariable.index); + } + } + } + } + + + mv.visitMethodInsn(Opcodes.INVOKESTATIC, + fragmentClassName, + conditionCheckMethodName, + instrumentationBreakpointInfo.methodSignature, + false); + mv.visitLabel(endTry); + mv.visitJumpInsn(Opcodes.GOTO, afterIf); + + mv.visitLabel(catchBlock); + if (instrumentationId >= Short.MAX_VALUE) { + throw new IllegalArgumentException("Instrumentation ID exceeds short range: " + instrumentationId); + } + mv.visitIntInsn(Opcodes.SIPUSH, instrumentationId); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, + getInternalClsName(ConditionalBreakpointTransformer.class), + "instrumentationException", + "(Ljava/lang/Throwable;I)V", + false); + + mv.visitLabel(afterIf); + } catch (Throwable e) { + throw new InstrumentationBpExceptionWrapper(e, instrumentationId); + } + } + }; + } + }, 0, true); + } catch (Throwable e) { + if (e instanceof InstrumentationBpExceptionWrapper) { + instrumentationFailed(e.getCause(), ((InstrumentationBpExceptionWrapper)e).instrumentationId); + } else { + instrumentationFailed(e, -1); + } + } + return null; + } + } + + private static Map getLineNumbers(String methodName, Map> methods) { + int index = methodName.indexOf("$lambda$"); + if (index > 0) { + String whereMethodName = methodName.substring(0, index); + return methods.get(whereMethodName); + } + return methods.get(methodName); + } + + @SuppressWarnings("unused") + public static void instrumentationFailed(Throwable e, int instrumentationId) { + // The report will be on the IDE side by a special breakpoint + } + + + @SuppressWarnings("unused") + public static void instrumentationException(Throwable e, int instrumentationId) { + // The report will be on the IDE side by a special breakpoint + } + + private static int extractIdFromFragmentClassName(String fragmentClassName) { + // Find the last digit sequence in the class name + int i = fragmentClassName.length() - 1; + while (i >= 0 && Character.isDigit(fragmentClassName.charAt(i))) { + i--; + } + if (i < fragmentClassName.length() - 1) { + return Integer.parseInt(fragmentClassName.substring(i + 1)); + } + throw new IllegalArgumentException("Fragment class name does not contain a valid instrumentation ID: " + fragmentClassName); + } +} diff --git a/src/main/java/com/intellij/rt/debugger/agent/DebuggerAgent.java b/src/main/java/com/intellij/rt/debugger/agent/DebuggerAgent.java index aa0a422..e58dc06 100644 --- a/src/main/java/com/intellij/rt/debugger/agent/DebuggerAgent.java +++ b/src/main/java/com/intellij/rt/debugger/agent/DebuggerAgent.java @@ -32,6 +32,7 @@ private static void initAll(Instrumentation instrumentation, Properties properti SuspendHelper.init(properties); CollectionBreakpointInstrumentor.init(instrumentation); SpilledVariablesTransformer.init(instrumentation); + ConditionalBreakpointTransformer.init(properties, instrumentation); } private static void readAndApplyProperties(String uri, Instrumentation instrumentation) { From bf24a4c3a716a2b8a7dd6cc1fb45c371b99b41cb Mon Sep 17 00:00:00 2001 From: "Alexey.Merkulov" Date: Thu, 13 Nov 2025 18:40:23 +0100 Subject: [PATCH 2/8] Add mute breakpoints global flag --- .../agent/ConditionalBreakpointTransformer.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java b/src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java index 2876ef4..5df9d7b 100644 --- a/src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java +++ b/src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java @@ -137,6 +137,11 @@ private void addInstrumentationCondition(InstrumentationBreakpointInfo instrumen Label catchBlock = new Label(); Label afterIf = new Label(); + String theTransformerClassName = getInternalClsName(ConditionalBreakpointTransformer.class); + + mv.visitFieldInsn(Opcodes.GETSTATIC, theTransformerClassName, "isMutedState", "Z"); + mv.visitJumpInsn(Opcodes.IFNE, afterIf); + mv.visitTryCatchBlock(startTry, endTry, catchBlock, "java/lang/Throwable"); mv.visitLabel(startTry); @@ -172,7 +177,7 @@ private void addInstrumentationCondition(InstrumentationBreakpointInfo instrumen } mv.visitIntInsn(Opcodes.SIPUSH, instrumentationId); mv.visitMethodInsn(Opcodes.INVOKESTATIC, - getInternalClsName(ConditionalBreakpointTransformer.class), + theTransformerClassName, "instrumentationException", "(Ljava/lang/Throwable;I)V", false); @@ -205,6 +210,10 @@ private static Map getLineNumbers(String return methods.get(methodName); } + /** This field is changing by the Debugger Engine side and used inside instrumented code */ + @SuppressWarnings("unused") + public static boolean isMutedState = false; + @SuppressWarnings("unused") public static void instrumentationFailed(Throwable e, int instrumentationId) { // The report will be on the IDE side by a special breakpoint From f2a9cdd70e8b30e3d8a13015650693d98ff61d27 Mon Sep 17 00:00:00 2001 From: "Alexey.Merkulov" Date: Mon, 17 Nov 2025 18:03:38 +0100 Subject: [PATCH 3/8] Implement preventing recursion for instrumented breakpoints and provide information about conditional breakpoint check to engine --- .../ConditionalBreakpointTransformer.java | 57 ++++++++++++++++++- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java b/src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java index 5df9d7b..18407d4 100644 --- a/src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java +++ b/src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java @@ -136,11 +136,17 @@ private void addInstrumentationCondition(InstrumentationBreakpointInfo instrumen Label endTry = new Label(); Label catchBlock = new Label(); Label afterIf = new Label(); + Label checkIsDone = new Label(); String theTransformerClassName = getInternalClsName(ConditionalBreakpointTransformer.class); - mv.visitFieldInsn(Opcodes.GETSTATIC, theTransformerClassName, "isMutedState", "Z"); - mv.visitJumpInsn(Opcodes.IFNE, afterIf); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, + theTransformerClassName, + "enterBreakpointCheck", + "()Z", + false); + + mv.visitJumpInsn(Opcodes.IFNE, checkIsDone); mv.visitTryCatchBlock(startTry, endTry, catchBlock, "java/lang/Throwable"); @@ -183,6 +189,13 @@ private void addInstrumentationCondition(InstrumentationBreakpointInfo instrumen false); mv.visitLabel(afterIf); + + mv.visitMethodInsn(Opcodes.INVOKESTATIC, + theTransformerClassName, + "checkIsDone", + "()V", + false); + mv.visitLabel(checkIsDone); } catch (Throwable e) { throw new InstrumentationBpExceptionWrapper(e, instrumentationId); } @@ -210,6 +223,46 @@ private static Map getLineNumbers(String return methods.get(methodName); } + private static final ThreadLocal myThreadLocal = new ThreadLocal<>(); + + /** + * This method is used from the Debugger Engine side to check + * that the triggered breakpoint is not met under some evaluation + */ + @SuppressWarnings("unused") + public static boolean isUnderBreakpointCheck() { + Integer previous = myThreadLocal.get(); + if (previous == null) { + previous = 0; + } + return previous > 0; + } + + /** This method is used from instrumented code */ + @SuppressWarnings("unused") + public static boolean enterBreakpointCheck() { + Integer previous = myThreadLocal.get(); + if (previous == null) { + previous = 0; + } + if (previous > 0) { + return true; + } + myThreadLocal.set(previous + 1); + + return isMutedState; + } + + /** This method is used from instrumented code */ + @SuppressWarnings("unused") + public static void checkIsDone() { + Integer previous = myThreadLocal.get(); + if (previous == null) { + previous = 0; + } + myThreadLocal.set(previous - 1); + } + /** This field is changing by the Debugger Engine side and used inside instrumented code */ @SuppressWarnings("unused") public static boolean isMutedState = false; From 8ba892288025a4f5ee62e04b2d636e22e9c38879 Mon Sep 17 00:00:00 2001 From: "Alexey.Merkulov" Date: Wed, 19 Nov 2025 19:34:03 +0100 Subject: [PATCH 4/8] Fix case with several variables with the same name in a method --- .../ConditionalBreakpointTransformer.java | 115 ++++++++++++++---- 1 file changed, 91 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java b/src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java index 18407d4..5ba50af 100644 --- a/src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java +++ b/src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java @@ -1,9 +1,7 @@ package com.intellij.rt.debugger.agent; import org.jetbrains.capture.org.objectweb.asm.*; -import org.jetbrains.capture.org.objectweb.asm.tree.ClassNode; -import org.jetbrains.capture.org.objectweb.asm.tree.LocalVariableNode; -import org.jetbrains.capture.org.objectweb.asm.tree.MethodNode; +import org.jetbrains.capture.org.objectweb.asm.tree.*; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.Instrumentation; @@ -35,6 +33,21 @@ class InstrumentationBreakpointInfo { } } + +interface ArgumentGetter { + void generateArgumentLoad(MethodVisitor mv); +} + +class InstrumentationBreakpointMappingInfo { + final InstrumentationBreakpointInfo inputInfo; + final List argumentLoadGenerators; + + InstrumentationBreakpointMappingInfo(InstrumentationBreakpointInfo inputInfo, List argumentLoadGenerators) { + this.inputInfo = inputInfo; + this.argumentLoadGenerators = argumentLoadGenerators; + } +} + public class ConditionalBreakpointTransformer { private static final String conditionCheckMethodName = "conditionCheck"; @@ -115,21 +128,26 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str if (m == null) { return superMethodVisitor; } - final MethodNode methodNode = m; + + final Map argumentMapping = collectArgumentMapping(m, lineNumbers); + + if (argumentMapping.isEmpty()) { + return superMethodVisitor; + } return new MethodVisitor(api, superMethodVisitor) { @Override public void visitLineNumber(int line, Label start) { - InstrumentationBreakpointInfo instrumentationBreakpointInfo = lineNumbers.get(line); + InstrumentationBreakpointMappingInfo instrumentationBreakpointInfo = argumentMapping.get(line); if (instrumentationBreakpointInfo != null) { - addInstrumentationCondition(instrumentationBreakpointInfo, start); + addInstrumentationCondition(instrumentationBreakpointInfo); } super.visitLineNumber(line, start); } - private void addInstrumentationCondition(InstrumentationBreakpointInfo instrumentationBreakpointInfo, Label start) { - String fragmentClassName = instrumentationBreakpointInfo.fragmentClassName; + private void addInstrumentationCondition(InstrumentationBreakpointMappingInfo argumentMapping) { + String fragmentClassName = argumentMapping.inputInfo.fragmentClassName; int instrumentationId = extractIdFromFragmentClassName(fragmentClassName); try { Label startTry = new Label(); @@ -152,27 +170,14 @@ private void addInstrumentationCondition(InstrumentationBreakpointInfo instrumen mv.visitLabel(startTry); - for (String argumentName : instrumentationBreakpointInfo.argumentNames) { - if (argumentName.equals("this")/* || argumentName.startsWith("this$")*/) { - mv.visitVarInsn(Opcodes.ALOAD, 0); - } - else { - for (LocalVariableNode localVariable : methodNode.localVariables) { - if (localVariable.name.equals(argumentName) /*&& - start.getOffset() >= localVariable.start.getLabel().getOffset() && - start.getOffset() < localVariable.end.getLabel().getOffset()*/) { - Type type = Type.getType(localVariable.desc); - mv.visitVarInsn(type.getOpcode(Opcodes.ILOAD), localVariable.index); - } - } - } + for (ArgumentGetter argumentLoadGenerator : argumentMapping.argumentLoadGenerators) { + argumentLoadGenerator.generateArgumentLoad(mv); } - mv.visitMethodInsn(Opcodes.INVOKESTATIC, fragmentClassName, conditionCheckMethodName, - instrumentationBreakpointInfo.methodSignature, + argumentMapping.inputInfo.methodSignature, false); mv.visitLabel(endTry); mv.visitJumpInsn(Opcodes.GOTO, afterIf); @@ -214,6 +219,63 @@ private void addInstrumentationCondition(InstrumentationBreakpointInfo instrumen } } + private static Map collectArgumentMapping(MethodNode method, Map lineNumbers) { + final Map remappingInfo = new HashMap<>(); + + Set visitedLineNumbers = new HashSet<>(); + for (int instructionIndex = 0; instructionIndex < method.instructions.size(); instructionIndex++) { + AbstractInsnNode instruction = method.instructions.get(instructionIndex); + if (!(instruction instanceof LineNumberNode)) { + continue; + } + + int lineNumber = ((LineNumberNode)instruction).line; + InstrumentationBreakpointInfo instrumentationBreakpointInfo = lineNumbers.get(lineNumber); + if (instrumentationBreakpointInfo != null) { + // skip non-trivial cases for now + boolean isFirstTimeMetLineNumber = visitedLineNumbers.add(lineNumber); + if (!isFirstTimeMetLineNumber) { + impossibleToInstrument("Several instructions marked with the same line " + lineNumber, extractIdFromFragmentClassName(instrumentationBreakpointInfo.fragmentClassName)); + remappingInfo.remove(lineNumber); + } + else { + List argumentLoadGenerators = new ArrayList<>(); + for (String argumentName : instrumentationBreakpointInfo.argumentNames) { + boolean isFound = false; + for (LocalVariableNode localVariableNode : method.localVariables) { + if (localVariableNode.name.equals(argumentName) && + method.instructions.indexOf(localVariableNode.start) <= instructionIndex && + instructionIndex < method.instructions.indexOf(localVariableNode.end) + ) { + isFound = true; + + final Type type = Type.getType(localVariableNode.desc); + final int localVariableIndex = localVariableNode.index; + argumentLoadGenerators.add(new ArgumentGetter() { + @Override + public void generateArgumentLoad(MethodVisitor mv) { + mv.visitVarInsn(type.getOpcode(Opcodes.ILOAD), localVariableIndex); + } + }); + break; + } + } + if (!isFound) { + impossibleToInstrument("Argument " + argumentName + " not found", extractIdFromFragmentClassName(instrumentationBreakpointInfo.fragmentClassName)); + break; + } + } + + if (argumentLoadGenerators.size() == instrumentationBreakpointInfo.argumentNames.size()) { + remappingInfo.put(lineNumber, new InstrumentationBreakpointMappingInfo(instrumentationBreakpointInfo, argumentLoadGenerators)); + } + } + } + } + + return remappingInfo; + } + private static Map getLineNumbers(String methodName, Map> methods) { int index = methodName.indexOf("$lambda$"); if (index > 0) { @@ -278,6 +340,11 @@ public static void instrumentationException(Throwable e, int instrumentationId) // The report will be on the IDE side by a special breakpoint } + @SuppressWarnings("unused") + public static void impossibleToInstrument(String message, int instrumentationId) { + // The report will be on the IDE side by a special breakpoint + } + private static int extractIdFromFragmentClassName(String fragmentClassName) { // Find the last digit sequence in the class name int i = fragmentClassName.length() - 1; From e4cb18e2fc34e8268be6742565e836dfd5eacece Mon Sep 17 00:00:00 2001 From: "Alexey.Merkulov" Date: Mon, 24 Nov 2025 19:05:06 +0100 Subject: [PATCH 5/8] Improve muting breakpoints --- .../ConditionalBreakpointTransformer.java | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java b/src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java index 5ba50af..f97f8a9 100644 --- a/src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java +++ b/src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java @@ -300,9 +300,17 @@ public static boolean isUnderBreakpointCheck() { return previous > 0; } - /** This method is used from instrumented code */ + /** + * This method is used from instrumented code + * + * @return true if the breakpoint condition should be skipped + */ @SuppressWarnings("unused") public static boolean enterBreakpointCheck() { + if (!isUnmutedState) { + return true; + } + Integer previous = myThreadLocal.get(); if (previous == null) { previous = 0; @@ -312,7 +320,7 @@ public static boolean enterBreakpointCheck() { } myThreadLocal.set(previous + 1); - return isMutedState; + return false; } /** This method is used from instrumented code */ @@ -325,9 +333,14 @@ public static void checkIsDone() { myThreadLocal.set(previous - 1); } - /** This field is changing by the Debugger Engine side and used inside instrumented code */ + /** + * This field is changing by the Debugger Engine side and used inside instrumented code + *

+ * Should not be initialized, so we can set it from Class Prepare Event from the Debugger Engine, + * and the `init` will not rewrite it + */ @SuppressWarnings("unused") - public static boolean isMutedState = false; + public static boolean isUnmutedState; @SuppressWarnings("unused") public static void instrumentationFailed(Throwable e, int instrumentationId) { From 25f9de638aa990cccb2d3270f70dea22b6ab0b4d Mon Sep 17 00:00:00 2001 From: "Alexey.Merkulov" Date: Tue, 25 Nov 2025 18:40:21 +0100 Subject: [PATCH 6/8] Rework exception reporting from instrumentation: report the string instead of exception object --- .../ConditionalBreakpointTransformer.java | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java b/src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java index f97f8a9..fb98533 100644 --- a/src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java +++ b/src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java @@ -3,6 +3,8 @@ import org.jetbrains.capture.org.objectweb.asm.*; import org.jetbrains.capture.org.objectweb.asm.tree.*; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain; @@ -183,14 +185,19 @@ private void addInstrumentationCondition(InstrumentationBreakpointMappingInfo ar mv.visitJumpInsn(Opcodes.GOTO, afterIf); mv.visitLabel(catchBlock); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, + "java/lang/Throwable", + "toString", + "()Ljava/lang/String;", + false); if (instrumentationId >= Short.MAX_VALUE) { throw new IllegalArgumentException("Instrumentation ID exceeds short range: " + instrumentationId); } mv.visitIntInsn(Opcodes.SIPUSH, instrumentationId); mv.visitMethodInsn(Opcodes.INVOKESTATIC, theTransformerClassName, - "instrumentationException", - "(Ljava/lang/Throwable;I)V", + "reportIncorrectInstrumentation", + "(Ljava/lang/String;I)V", false); mv.visitLabel(afterIf); @@ -342,14 +349,38 @@ public static void checkIsDone() { @SuppressWarnings("unused") public static boolean isUnmutedState; - @SuppressWarnings("unused") public static void instrumentationFailed(Throwable e, int instrumentationId) { - // The report will be on the IDE side by a special breakpoint + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + PrintStream s = new PrintStream(baos); + + s.println(e); + + StackTraceElement[] trace = e.getStackTrace(); + int lastToReportIndex = 0; + for (int i = 0; i < trace.length; i++) { + StackTraceElement traceElement = trace[i]; + if (traceElement.getClassName().startsWith("com.intellij.rt.debugger.agent")) { + lastToReportIndex = i; + } + } + for (int i = 0; i <= lastToReportIndex; i++) { + StackTraceElement traceElement = trace[i]; + s.println("\tat " + traceElement); + } + + reportInstrumentationFailed(baos.toString(), instrumentationId); + + s.close(); } + @SuppressWarnings("unused") + public static void reportInstrumentationFailed(String report, int instrumentationId) { + // The report will be on the IDE side by a special breakpoint + } @SuppressWarnings("unused") - public static void instrumentationException(Throwable e, int instrumentationId) { + public static void reportIncorrectInstrumentation(String report, int instrumentationId) { // The report will be on the IDE side by a special breakpoint } From 56bc7b24cbf64ec785deb4fc72c310167e2cc975 Mon Sep 17 00:00:00 2001 From: "Alexey.Merkulov" Date: Tue, 25 Nov 2025 19:21:15 +0100 Subject: [PATCH 7/8] Improve problems reporting --- .../debugger/agent/ConditionalBreakpointTransformer.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java b/src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java index fb98533..997440d 100644 --- a/src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java +++ b/src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java @@ -238,7 +238,10 @@ private static Map collectArgumen int lineNumber = ((LineNumberNode)instruction).line; InstrumentationBreakpointInfo instrumentationBreakpointInfo = lineNumbers.get(lineNumber); - if (instrumentationBreakpointInfo != null) { + if (instrumentationBreakpointInfo == null) { + continue; + } + try { // skip non-trivial cases for now boolean isFirstTimeMetLineNumber = visitedLineNumbers.add(lineNumber); if (!isFirstTimeMetLineNumber) { @@ -277,6 +280,8 @@ public void generateArgumentLoad(MethodVisitor mv) { remappingInfo.put(lineNumber, new InstrumentationBreakpointMappingInfo(instrumentationBreakpointInfo, argumentLoadGenerators)); } } + } catch (Throwable e) { + throw new InstrumentationBpExceptionWrapper(e, extractIdFromFragmentClassName(instrumentationBreakpointInfo.fragmentClassName)); } } From b6207d3f27c60588298b71d9ef5bb7338e2719a2 Mon Sep 17 00:00:00 2001 From: "Alexey.Merkulov" Date: Wed, 17 Dec 2025 16:31:45 +0100 Subject: [PATCH 8/8] Big instrumentation engine rework --- .../ConditionalBreakpointTransformer.java | 104 ++++++++++++------ 1 file changed, 71 insertions(+), 33 deletions(-) diff --git a/src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java b/src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java index 997440d..276305d 100644 --- a/src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java +++ b/src/main/java/com/intellij/rt/debugger/agent/ConditionalBreakpointTransformer.java @@ -22,12 +22,14 @@ class InstrumentationBpExceptionWrapper extends RuntimeException { } class InstrumentationBreakpointInfo { - final Integer lineNumber; + final int instrumentationId; + final int lineNumber; final String fragmentClassName; final String methodSignature; final List argumentNames; - InstrumentationBreakpointInfo(Integer lineNumber, String fragmentClassName, String methodSignature, List argumentNames) { + InstrumentationBreakpointInfo(int instrumentationId, int lineNumber, String fragmentClassName, String methodSignature, List argumentNames) { + this.instrumentationId = instrumentationId; this.lineNumber = lineNumber; this.fragmentClassName = fragmentClassName; this.methodSignature = methodSignature; @@ -53,7 +55,7 @@ class InstrumentationBreakpointMappingInfo { public class ConditionalBreakpointTransformer { private static final String conditionCheckMethodName = "conditionCheck"; - private static final Map>> myBreakpoints = new LinkedHashMap<>(); + private static final Set myClassesWithBreakpoints = new LinkedHashSet<>(); public static void init(Properties properties, Instrumentation instrumentation) { applyProperties(properties); @@ -67,44 +69,58 @@ private static void applyProperties(Properties properties) { } private static void addPoint(String propertyKey, String propertyValue) { - if (propertyKey.startsWith("instrumentation.condition.breakpoint")) { - String[] split = propertyValue.split(" "); - if (split.length >= 4) { - addBreakpoint(Arrays.asList(split)); - } + if (propertyKey.startsWith("instrumentation.breakpoint.class")) { + addBreakpointClass(propertyValue); } } - public static void addBreakpoint(List split) { - String className = split.get(0); - String methodName = split.get(1); - int lineNumber = Integer.parseInt(split.get(2)); - String fragmentClassName = split.get(3); - String methodSignature = split.get(4); - List argumentNames = split.subList(5, split.size()); - - Map> methods = myBreakpoints.get(className); - if (methods == null) { - methods = new LinkedHashMap<>(); - myBreakpoints.put(className, methods); - } - Map lineNumbers = getLineNumbers(methodName, methods); - if (lineNumbers == null) { - lineNumbers = new LinkedHashMap<>(); - methods.put(methodName, lineNumbers); - } - lineNumbers.put(lineNumber, new InstrumentationBreakpointInfo(lineNumber, fragmentClassName, methodSignature, argumentNames)); + public static void addBreakpointClass(String className) { + myClassesWithBreakpoints.add(className); } private static class BreakpointInstrumentalist implements ClassFileTransformer { @Override public byte[] transform(final ClassLoader loader, final String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { - final Map> methods = myBreakpoints.get(className); - if (methods == null || methods.isEmpty()) { + if (!myClassesWithBreakpoints.contains(className)) { return null; } + final List successIds = new ArrayList<>(); + try { + String[] megaInfo = requestInstrumentedInfo(className); + if (megaInfo.length == 0) { + return null; + } + + final Map> methods = new LinkedHashMap<>(); + for (String info : megaInfo) { + List split = Arrays.asList(info.split(" ")); + String className2 = split.get(0); + if (!className2.equals(className)) { + // TODO: ERROR here + continue; + } + String methodName = split.get(1); + int lineNumber = Integer.parseInt(split.get(2)); + String fragmentClassName = split.get(3); + String methodSignature = split.get(4); + List argumentNames = split.subList(5, split.size()); + + Map lineNumbers = getLineNumbers(methodName, methods); + if (lineNumbers == null) { + lineNumbers = new LinkedHashMap<>(); + methods.put(methodName, lineNumbers); + } + + int instrumentationId = extractIdFromFragmentClassName(fragmentClassName); + lineNumbers.put(lineNumber, new InstrumentationBreakpointInfo(instrumentationId, lineNumber, fragmentClassName, methodSignature, argumentNames)); + } + + if (methods.isEmpty()) { + return null; + } + final ClassNode classNode = new ClassNode(); new ClassReader(classfileBuffer).accept(classNode, 0); @@ -112,6 +128,7 @@ public byte[] transform(final ClassLoader loader, final String className, Class< className, classfileBuffer, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS, loader ); + return transformer.accept(new ClassVisitor(Opcodes.API_VERSION, transformer.writer) { @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { @@ -150,7 +167,10 @@ public void visitLineNumber(int line, Label start) { private void addInstrumentationCondition(InstrumentationBreakpointMappingInfo argumentMapping) { String fragmentClassName = argumentMapping.inputInfo.fragmentClassName; - int instrumentationId = extractIdFromFragmentClassName(fragmentClassName); + int instrumentationId = argumentMapping.inputInfo.instrumentationId; + + successIds.add(instrumentationId); + try { Label startTry = new Label(); Label endTry = new Label(); @@ -221,6 +241,15 @@ private void addInstrumentationCondition(InstrumentationBreakpointMappingInfo ar } else { instrumentationFailed(e, -1); } + } finally { + Integer[] integerArray = successIds.toArray(new Integer[0]); + + int[] intArray = new int[integerArray.length]; + for (int i = 0; i < integerArray.length; i++) { + intArray[i] = integerArray[i]; + } + + successfullyInstrumented(loader, intArray); } return null; } @@ -245,7 +274,7 @@ private static Map collectArgumen // skip non-trivial cases for now boolean isFirstTimeMetLineNumber = visitedLineNumbers.add(lineNumber); if (!isFirstTimeMetLineNumber) { - impossibleToInstrument("Several instructions marked with the same line " + lineNumber, extractIdFromFragmentClassName(instrumentationBreakpointInfo.fragmentClassName)); + impossibleToInstrument("Several instructions marked with the same line " + lineNumber, instrumentationBreakpointInfo.instrumentationId); remappingInfo.remove(lineNumber); } else { @@ -271,7 +300,7 @@ public void generateArgumentLoad(MethodVisitor mv) { } } if (!isFound) { - impossibleToInstrument("Argument " + argumentName + " not found", extractIdFromFragmentClassName(instrumentationBreakpointInfo.fragmentClassName)); + impossibleToInstrument("Argument " + argumentName + " not found", instrumentationBreakpointInfo.instrumentationId); break; } } @@ -281,7 +310,7 @@ public void generateArgumentLoad(MethodVisitor mv) { } } } catch (Throwable e) { - throw new InstrumentationBpExceptionWrapper(e, extractIdFromFragmentClassName(instrumentationBreakpointInfo.fragmentClassName)); + throw new InstrumentationBpExceptionWrapper(e, instrumentationBreakpointInfo.instrumentationId); } } @@ -379,6 +408,15 @@ public static void instrumentationFailed(Throwable e, int instrumentationId) { s.close(); } + public static String[] requestInstrumentedInfo(@SuppressWarnings("unused") String className) { + return new String[0]; + } + + @SuppressWarnings("unused") + public static void successfullyInstrumented(ClassLoader loader, int[] instrumentationIds) { + // The report will be on the IDE side by a special breakpoint + } + @SuppressWarnings("unused") public static void reportInstrumentationFailed(String report, int instrumentationId) { // The report will be on the IDE side by a special breakpoint