Skip to content

Commit a3dd3be

Browse files
authored
Merge pull request #1543 from codeflash-ai/fix/java/line-profiler
feat: implement line-level profiling agent with ASM instrumentation
2 parents efb5cd9 + 5447c83 commit a3dd3be

19 files changed

Lines changed: 2864 additions & 488 deletions

codeflash-java-runtime/pom.xml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@
4848
<version>3.45.0.0</version>
4949
</dependency>
5050

51+
<!-- ASM for bytecode instrumentation (profiler agent) -->
52+
<dependency>
53+
<groupId>org.ow2.asm</groupId>
54+
<artifactId>asm</artifactId>
55+
<version>9.7.1</version>
56+
</dependency>
57+
<dependency>
58+
<groupId>org.ow2.asm</groupId>
59+
<artifactId>asm-commons</artifactId>
60+
<version>9.7.1</version>
61+
</dependency>
62+
5163
<!-- JUnit 5 for testing -->
5264
<dependency>
5365
<groupId>org.junit.jupiter</groupId>
@@ -100,9 +112,19 @@
100112
<goal>shade</goal>
101113
</goals>
102114
<configuration>
115+
<relocations>
116+
<relocation>
117+
<pattern>org.objectweb.asm</pattern>
118+
<shadedPattern>com.codeflash.asm</shadedPattern>
119+
</relocation>
120+
</relocations>
103121
<transformers>
104122
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
105123
<mainClass>com.codeflash.Comparator</mainClass>
124+
<manifestEntries>
125+
<Premain-Class>com.codeflash.profiler.ProfilerAgent</Premain-Class>
126+
<Can-Retransform-Classes>true</Can-Retransform-Classes>
127+
</manifestEntries>
106128
</transformer>
107129
</transformers>
108130
<filters>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.codeflash.profiler;
2+
3+
import org.objectweb.asm.ClassVisitor;
4+
import org.objectweb.asm.MethodVisitor;
5+
import org.objectweb.asm.Opcodes;
6+
7+
/**
8+
* ASM ClassVisitor that filters methods and wraps target methods with
9+
* {@link LineProfilingMethodVisitor} for line-level profiling.
10+
*/
11+
public class LineProfilingClassVisitor extends ClassVisitor {
12+
13+
private final String internalClassName;
14+
private final ProfilerConfig config;
15+
private String sourceFile;
16+
17+
public LineProfilingClassVisitor(ClassVisitor classVisitor, String internalClassName, ProfilerConfig config) {
18+
super(Opcodes.ASM9, classVisitor);
19+
this.internalClassName = internalClassName;
20+
this.config = config;
21+
}
22+
23+
@Override
24+
public void visitSource(String source, String debug) {
25+
super.visitSource(source, debug);
26+
// Resolve the absolute source file path from the config
27+
this.sourceFile = config.resolveSourceFile(internalClassName);
28+
}
29+
30+
@Override
31+
public MethodVisitor visitMethod(int access, String name, String descriptor,
32+
String signature, String[] exceptions) {
33+
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
34+
35+
if (config.shouldInstrumentMethod(internalClassName, name)) {
36+
return new LineProfilingMethodVisitor(mv, access, name, descriptor,
37+
internalClassName, sourceFile);
38+
}
39+
return mv;
40+
}
41+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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 &lt; 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+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.codeflash.profiler;
2+
3+
import org.objectweb.asm.ClassReader;
4+
import org.objectweb.asm.ClassWriter;
5+
6+
import java.lang.instrument.ClassFileTransformer;
7+
import java.security.ProtectionDomain;
8+
9+
/**
10+
* {@link ClassFileTransformer} that instruments target classes with line profiling.
11+
*
12+
* <p>When a class matches the profiler configuration, it is run through ASM
13+
* to inject {@link ProfilerData#hit(int)} calls at each line number.
14+
*/
15+
public class LineProfilingTransformer implements ClassFileTransformer {
16+
17+
private final ProfilerConfig config;
18+
19+
public LineProfilingTransformer(ProfilerConfig config) {
20+
this.config = config;
21+
}
22+
23+
@Override
24+
public byte[] transform(ClassLoader loader, String className,
25+
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
26+
byte[] classfileBuffer) {
27+
if (className == null || !config.shouldInstrumentClass(className)) {
28+
return null; // null = don't transform
29+
}
30+
31+
try {
32+
return instrumentClass(className, classfileBuffer);
33+
} catch (Exception e) {
34+
System.err.println("[codeflash-profiler] Failed to instrument " + className + ": " + e.getMessage());
35+
return null;
36+
}
37+
}
38+
39+
private byte[] instrumentClass(String internalClassName, byte[] bytecode) {
40+
ClassReader cr = new ClassReader(bytecode);
41+
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
42+
LineProfilingClassVisitor cv = new LineProfilingClassVisitor(cw, internalClassName, config);
43+
cr.accept(cv, ClassReader.EXPAND_FRAMES);
44+
return cw.toByteArray();
45+
}
46+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.codeflash.profiler;
2+
3+
import java.lang.instrument.Instrumentation;
4+
5+
/**
6+
* Java agent entry point for the CodeFlash line profiler.
7+
*
8+
* <p>Loaded via {@code -javaagent:codeflash-profiler-agent.jar=config=/path/to/config.json}.
9+
*
10+
* <p>The agent:
11+
* <ol>
12+
* <li>Parses the config file specifying which classes/methods to profile</li>
13+
* <li>Registers a {@link LineProfilingTransformer} to instrument target classes at load time</li>
14+
* <li>Registers a shutdown hook to write profiling results to JSON</li>
15+
* </ol>
16+
*/
17+
public class ProfilerAgent {
18+
19+
/**
20+
* Called by the JVM before {@code main()} when the agent is loaded.
21+
*
22+
* @param agentArgs comma-separated key=value pairs (e.g., {@code config=/path/to/config.json})
23+
* @param inst the JVM instrumentation interface
24+
*/
25+
public static void premain(String agentArgs, Instrumentation inst) {
26+
ProfilerConfig config = ProfilerConfig.parse(agentArgs);
27+
28+
if (config.getTargetClasses().isEmpty()) {
29+
System.err.println("[codeflash-profiler] No target classes configured, profiler inactive");
30+
return;
31+
}
32+
33+
// Pre-allocate registry with estimated capacity
34+
ProfilerRegistry.initialize(config.getExpectedLineCount());
35+
36+
// Configure warmup phase
37+
ProfilerData.setWarmupThreshold(config.getWarmupIterations());
38+
39+
// Register the bytecode transformer
40+
inst.addTransformer(new LineProfilingTransformer(config), true);
41+
42+
// Register shutdown hook to write results on JVM exit
43+
String outputFile = config.getOutputFile();
44+
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
45+
ProfilerReporter.writeResults(outputFile, config);
46+
}, "codeflash-profiler-shutdown"));
47+
48+
int warmup = config.getWarmupIterations();
49+
String warmupMsg = warmup > 0 ? ", warmup=" + warmup + " calls" : "";
50+
System.err.println("[codeflash-profiler] Agent loaded, profiling "
51+
+ config.getTargetClasses().size() + " class(es)" + warmupMsg);
52+
}
53+
}

0 commit comments

Comments
 (0)