|
| 1 | +package com.codeflash.profiler; |
| 2 | + |
| 3 | +import org.objectweb.asm.Label; |
| 4 | +import org.objectweb.asm.MethodVisitor; |
| 5 | +import org.objectweb.asm.Opcodes; |
| 6 | +import org.objectweb.asm.Type; |
| 7 | +import org.objectweb.asm.commons.AdviceAdapter; |
| 8 | + |
| 9 | +/** |
| 10 | + * ASM MethodVisitor that injects line-level profiling probes. |
| 11 | + * |
| 12 | + * <p>At each {@code LineNumber} table entry within the target method: |
| 13 | + * <ol> |
| 14 | + * <li>Registers the line with {@link ProfilerRegistry} (happens once at class-load time)</li> |
| 15 | + * <li>Injects bytecode: {@code LDC globalId; INVOKESTATIC ProfilerData.hit(I)V}</li> |
| 16 | + * </ol> |
| 17 | + * |
| 18 | + * <p>At method entry: injects a warmup self-call loop (if warmup is configured) followed by |
| 19 | + * {@code ProfilerData.enterMethod(entryLineId)}. |
| 20 | + * <p>At method exit (every RETURN/ATHROW): injects {@code ProfilerData.exitMethod()}. |
| 21 | + */ |
| 22 | +public class LineProfilingMethodVisitor extends AdviceAdapter { |
| 23 | + |
| 24 | + private static final String PROFILER_DATA = "com/codeflash/profiler/ProfilerData"; |
| 25 | + |
| 26 | + private final String internalClassName; |
| 27 | + private final String sourceFile; |
| 28 | + private final String methodName; |
| 29 | + private boolean firstLineVisited = false; |
| 30 | + |
| 31 | + protected LineProfilingMethodVisitor( |
| 32 | + MethodVisitor mv, int access, String name, String descriptor, |
| 33 | + String internalClassName, String sourceFile) { |
| 34 | + super(Opcodes.ASM9, mv, access, name, descriptor); |
| 35 | + this.internalClassName = internalClassName; |
| 36 | + this.sourceFile = sourceFile; |
| 37 | + this.methodName = name; |
| 38 | + } |
| 39 | + |
| 40 | + /** |
| 41 | + * Inject a warmup self-call loop at method entry. |
| 42 | + * |
| 43 | + * <p>Generated bytecode equivalent: |
| 44 | + * <pre> |
| 45 | + * if (ProfilerData.isWarmupNeeded()) { |
| 46 | + * ProfilerData.startWarmup(); |
| 47 | + * for (int i = 0; i < ProfilerData.getWarmupThreshold(); i++) { |
| 48 | + * thisMethod(originalArgs); |
| 49 | + * } |
| 50 | + * ProfilerData.finishWarmup(); |
| 51 | + * } |
| 52 | + * </pre> |
| 53 | + * |
| 54 | + * <p>Recursive warmup calls re-enter this method but {@code isWarmupNeeded()} returns |
| 55 | + * {@code false} (guard flag set by {@code startWarmup()}), so they execute the normal |
| 56 | + * instrumented body. After the loop, {@code finishWarmup()} zeros all counters so the |
| 57 | + * next real execution records clean data. |
| 58 | + */ |
| 59 | + @Override |
| 60 | + protected void onMethodEnter() { |
| 61 | + Label skipWarmup = new Label(); |
| 62 | + |
| 63 | + // if (!ProfilerData.isWarmupNeeded()) goto skipWarmup |
| 64 | + mv.visitMethodInsn(INVOKESTATIC, PROFILER_DATA, "isWarmupNeeded", "()Z", false); |
| 65 | + mv.visitJumpInsn(IFEQ, skipWarmup); |
| 66 | + |
| 67 | + // ProfilerData.startWarmup() |
| 68 | + mv.visitMethodInsn(INVOKESTATIC, PROFILER_DATA, "startWarmup", "()V", false); |
| 69 | + |
| 70 | + // int _warmupIdx = 0 |
| 71 | + int counterLocal = newLocal(Type.INT_TYPE); |
| 72 | + mv.visitInsn(ICONST_0); |
| 73 | + mv.visitVarInsn(ISTORE, counterLocal); |
| 74 | + |
| 75 | + Label loopCheck = new Label(); |
| 76 | + Label loopBody = new Label(); |
| 77 | + |
| 78 | + mv.visitJumpInsn(GOTO, loopCheck); |
| 79 | + |
| 80 | + // loop body: call self with original arguments |
| 81 | + mv.visitLabel(loopBody); |
| 82 | + |
| 83 | + boolean isStatic = (methodAccess & Opcodes.ACC_STATIC) != 0; |
| 84 | + if (!isStatic) { |
| 85 | + loadThis(); |
| 86 | + } |
| 87 | + loadArgs(); |
| 88 | + |
| 89 | + int invokeOp; |
| 90 | + if (isStatic) { |
| 91 | + invokeOp = INVOKESTATIC; |
| 92 | + } else if ((methodAccess & Opcodes.ACC_PRIVATE) != 0) { |
| 93 | + invokeOp = INVOKESPECIAL; |
| 94 | + } else { |
| 95 | + invokeOp = INVOKEVIRTUAL; |
| 96 | + } |
| 97 | + mv.visitMethodInsn(invokeOp, internalClassName, methodName, methodDesc, false); |
| 98 | + |
| 99 | + // Discard return value |
| 100 | + Type returnType = Type.getReturnType(methodDesc); |
| 101 | + switch (returnType.getSort()) { |
| 102 | + case Type.VOID: |
| 103 | + break; |
| 104 | + case Type.LONG: |
| 105 | + case Type.DOUBLE: |
| 106 | + mv.visitInsn(POP2); |
| 107 | + break; |
| 108 | + default: |
| 109 | + mv.visitInsn(POP); |
| 110 | + break; |
| 111 | + } |
| 112 | + |
| 113 | + // _warmupIdx++ |
| 114 | + mv.visitIincInsn(counterLocal, 1); |
| 115 | + |
| 116 | + // loop check: _warmupIdx < ProfilerData.getWarmupThreshold() |
| 117 | + mv.visitLabel(loopCheck); |
| 118 | + mv.visitVarInsn(ILOAD, counterLocal); |
| 119 | + mv.visitMethodInsn(INVOKESTATIC, PROFILER_DATA, "getWarmupThreshold", "()I", false); |
| 120 | + mv.visitJumpInsn(IF_ICMPLT, loopBody); |
| 121 | + |
| 122 | + // ProfilerData.finishWarmup() |
| 123 | + mv.visitMethodInsn(INVOKESTATIC, PROFILER_DATA, "finishWarmup", "()V", false); |
| 124 | + |
| 125 | + mv.visitLabel(skipWarmup); |
| 126 | + } |
| 127 | + |
| 128 | + @Override |
| 129 | + public void visitLineNumber(int line, Label start) { |
| 130 | + super.visitLineNumber(line, start); |
| 131 | + |
| 132 | + // Register this line and get its global ID (happens once at class-load time) |
| 133 | + String dotClassName = internalClassName.replace('/', '.'); |
| 134 | + int globalId = ProfilerRegistry.register(sourceFile, dotClassName, methodName, line); |
| 135 | + |
| 136 | + if (!firstLineVisited) { |
| 137 | + firstLineVisited = true; |
| 138 | + // Inject enterMethod call at the first line of the method |
| 139 | + mv.visitLdcInsn(globalId); |
| 140 | + mv.visitMethodInsn(INVOKESTATIC, PROFILER_DATA, "enterMethod", "(I)V", false); |
| 141 | + } |
| 142 | + |
| 143 | + // Inject: ProfilerData.hit(globalId) |
| 144 | + mv.visitLdcInsn(globalId); |
| 145 | + mv.visitMethodInsn(INVOKESTATIC, PROFILER_DATA, "hit", "(I)V", false); |
| 146 | + } |
| 147 | + |
| 148 | + @Override |
| 149 | + protected void onMethodExit(int opcode) { |
| 150 | + // Before every RETURN or ATHROW, flush timing for the last line |
| 151 | + // This fixes the "last line always shows 0ms" bug |
| 152 | + mv.visitMethodInsn(INVOKESTATIC, PROFILER_DATA, "exitMethod", "()V", false); |
| 153 | + } |
| 154 | +} |
0 commit comments