package org.intrace.agent; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.commons.EmptyVisitor; /** * InTrace uses ASM to instrument class files. However, ASM traverses bytecode * linearly which means some information is not available when we need it. This * class implements a first pass analysis phase to collect information which we * can't collect during the transformation phase. * <p> * This analysis collects three sets of data. * <ul> * <li>Reverse GOTO Lines * <li>Method Entry Line * <li>Method Argument Names * </ul> * <h1>Reverse GOTO Lines</h1> This analysis records the target line number of * GOTOs that jump backwards in the code. * * <h2>Example:</h2> * * <pre> * 1. public void method() * 2. { * 3. do * 4. { * 5. // Branch C * 6. // Branch C * 7. } * 8. while (condition) * 9. } * </pre> * * We want to capture line 5 and line 9. Example 1 gets transformed into the * following bytecode: * * <pre> * 5. label1: * 5. // Branch C * 6. // Branch C * 8. if (condition) goto label1 * </pre> * * <h3>Line 5:</h3> * <ul> * <li>{@link ClassAnalysis#visitLineNumber(int, Label)} is called and we record * a mapping from label1 to line 5. * </ul> * * <h3>Line 8:</h3> * <ul> * <li>{@link ClassAnalysis#visitJumpInsn(int, Label)} is called with a GOTO * instruction so we check whether we have already seen the target label. In * this case, we have so we mark the target line number as a reverse GOTO. * </ul> * * <h1>Method Entry Line</h1> This analysis records the first source line of a * method. This is necessary so that the transformation phase can add an entry * trace line in the call to {@link InstrumentedMethodWriter#visitCode()} and * know the source line. * * <h1>Method Argument Names</h1> This analysis records the variable names of * methods if available. * <p> * This analysis assumes that for a method with N arguments, the first * N local variables will be the method arguments. For non-static methods * we use local variables 1 -> N+1 to skip over the "this" variable. * <p> * This assumption might be wrong sometimes. Section 4.7.9 of the Class * File format definition makes the following statement "If LocalVariableTable * attributes are present in the attributes table of a given Code attribute, * then they may appear in any order.". However, testing on the SUN JVM * suggests that the local variables are visited in the order they were * defined in the source code. This allows the analysis code to be * much simpler. * <p> * The javadoc for Instrumentation.retransformClasses(...) states that * "Some attributes may not be present." in the bytecode which is * supplied for retransformation. On the Sun JVM this seems to mean * that we only get the Local Variable names when we are first transforming * a class and not when we retransform classes. */ public class ClassAnalysis extends EmptyVisitor { // Output of this analysis public final Map<String, Set<Integer>> methodReverseGOTOLines = new HashMap<String, Set<Integer>>(1); public final Map<String, Integer> methodEntryLines = new HashMap<String, Integer>(1); public final Map<String, List<String>> methodArgNames = new HashMap<String, List<String>>(1); // Intermediate fields private Set<Integer> currentMethod_reverseGOTOLines = new HashSet<Integer>(1); private final Map<Label, Integer> currentMethod_labelLineNos = new HashMap<Label, Integer>(1); private String currentMethod_sig; private boolean currentMethod_recordedEntryLine = false; private int currentMethod_numArgs = 0; private boolean currentMethod_skipArg; @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { currentMethod_sig = name + desc; currentMethod_recordedEntryLine = false; methodArgNames.put(currentMethod_sig, new ArrayList<String>(1)); Type[] argTypes = Type.getArgumentTypes(desc); currentMethod_numArgs = argTypes.length; currentMethod_skipArg = ((access & Opcodes.ACC_STATIC) == 0); if (currentMethod_skipArg) { currentMethod_numArgs++; } currentMethod_reverseGOTOLines = new HashSet<Integer>(1); currentMethod_labelLineNos.clear(); return this; } @Override public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) { if (currentMethod_skipArg) { currentMethod_skipArg = false; currentMethod_numArgs--; } else { if (currentMethod_numArgs > 0) { List<String> args = methodArgNames.get(currentMethod_sig); if (args != null) { // Do this in a null check block because we hit // an NPE here once. This shouldn't be possible // but this null check will avoid any problems. args.add(name); currentMethod_numArgs--; } } } } @Override public void visitLineNumber(int xiLineNo, Label xiLabel) { if (!currentMethod_recordedEntryLine) { methodEntryLines.put(currentMethod_sig, xiLineNo); currentMethod_recordedEntryLine = true; } currentMethod_labelLineNos.put(xiLabel, xiLineNo); } @Override public void visitJumpInsn(int xiOpCode, Label xiBranchLabel) { Integer lineNo = currentMethod_labelLineNos.get(xiBranchLabel); if (lineNo != null) { currentMethod_reverseGOTOLines.add(lineNo); } } @Override public void visitEnd() { List<String> methodLocalVars = methodArgNames.get(currentMethod_sig); if ((methodLocalVars != null) && (methodLocalVars.size() == 0)) { methodArgNames.remove(currentMethod_sig); } methodReverseGOTOLines.put(currentMethod_sig, currentMethod_reverseGOTOLines); currentMethod_sig = null; currentMethod_labelLineNos.clear(); } }