From c699093a26977052ccbb7ebcd626a5d872e76dcb Mon Sep 17 00:00:00 2001 From: misrasaurabh1 Date: Wed, 18 Mar 2026 23:03:35 -0700 Subject: [PATCH 01/12] Initial e2e tracer implementation --- .github/workflows/e2e-java-tracer.yaml | 98 +++++ .../java/com/codeflash/AgentDispatcher.java | 9 +- .../main/java/com/codeflash/ReplayHelper.java | 116 ++++++ .../com/codeflash/tracer/TraceRecorder.java | 123 +++++++ .../com/codeflash/tracer/TraceWriter.java | 210 +++++++++++ .../com/codeflash/tracer/TracerAgent.java | 26 ++ .../com/codeflash/tracer/TracerConfig.java | 113 ++++++ .../codeflash/tracer/TracingClassVisitor.java | 43 +++ .../tracer/TracingMethodAdapter.java | 132 +++++++ .../codeflash/tracer/TracingTransformer.java | 65 ++++ codeflash/benchmarking/function_ranker.py | 98 +++++ codeflash/discovery/discover_unit_tests.py | 14 +- codeflash/discovery/functions_to_optimize.py | 73 ++++ codeflash/languages/base.py | 1 + codeflash/languages/java/jfr_parser.py | 180 +++++++++ codeflash/languages/java/replay_test.py | 123 +++++++ .../resources/codeflash-runtime-1.0.0.jar | Bin 15952066 -> 15974015 bytes codeflash/languages/java/test_discovery.py | 72 +++- codeflash/languages/java/tracer.py | 167 +++++++++ codeflash/optimization/optimizer.py | 41 ++- codeflash/tracer.py | 177 +++++++-- codeflash/version.py | 2 +- tests/scripts/end_to_end_test_java_tracer.py | 127 +++++++ .../fixtures/java_tracer_e2e/codeflash.toml | 6 + .../fixtures/java_tracer_e2e/pom.xml | 67 ++++ .../src/main/java/com/example/Workload.java | 80 ++++ .../test_java/test_java_tracer_e2e.py | 304 +++++++++++++++ .../test_java/test_java_tracer_integration.py | 345 ++++++++++++++++++ .../test_java/test_test_discovery.py | 84 +++++ 29 files changed, 2861 insertions(+), 35 deletions(-) create mode 100644 .github/workflows/e2e-java-tracer.yaml create mode 100644 codeflash-java-runtime/src/main/java/com/codeflash/ReplayHelper.java create mode 100644 codeflash-java-runtime/src/main/java/com/codeflash/tracer/TraceRecorder.java create mode 100644 codeflash-java-runtime/src/main/java/com/codeflash/tracer/TraceWriter.java create mode 100644 codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracerAgent.java create mode 100644 codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracerConfig.java create mode 100644 codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingClassVisitor.java create mode 100644 codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingMethodAdapter.java create mode 100644 codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingTransformer.java create mode 100644 codeflash/languages/java/jfr_parser.py create mode 100644 codeflash/languages/java/replay_test.py create mode 100644 codeflash/languages/java/tracer.py create mode 100644 tests/scripts/end_to_end_test_java_tracer.py create mode 100644 tests/test_languages/fixtures/java_tracer_e2e/codeflash.toml create mode 100644 tests/test_languages/fixtures/java_tracer_e2e/pom.xml create mode 100644 tests/test_languages/fixtures/java_tracer_e2e/src/main/java/com/example/Workload.java create mode 100644 tests/test_languages/test_java/test_java_tracer_e2e.py create mode 100644 tests/test_languages/test_java/test_java_tracer_integration.py diff --git a/.github/workflows/e2e-java-tracer.yaml b/.github/workflows/e2e-java-tracer.yaml new file mode 100644 index 000000000..7e92e9eee --- /dev/null +++ b/.github/workflows/e2e-java-tracer.yaml @@ -0,0 +1,98 @@ +name: E2E - Java Tracer + +on: + pull_request: + paths: + - 'codeflash/languages/java/**' + - 'codeflash/languages/base.py' + - 'codeflash/languages/registry.py' + - 'codeflash/tracer.py' + - 'codeflash/benchmarking/function_ranker.py' + - 'codeflash/discovery/functions_to_optimize.py' + - 'codeflash/optimization/**' + - 'codeflash/verification/**' + - 'codeflash-java-runtime/**' + - 'tests/test_languages/fixtures/java_tracer_e2e/**' + - 'tests/scripts/end_to_end_test_java_tracer.py' + - '.github/workflows/e2e-java-tracer.yaml' + + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: true + +jobs: + java-tracer-e2e: + environment: ${{ (github.event_name == 'workflow_dispatch' || (contains(toJSON(github.event.pull_request.files.*.filename), '.github/workflows/') && github.event.pull_request.user.login != 'misrasaurabh1' && github.event.pull_request.user.login != 'KRRT7')) && 'external-trusted-contributors' || '' }} + + runs-on: ubuntu-latest + env: + CODEFLASH_AIS_SERVER: prod + POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }} + CODEFLASH_API_KEY: ${{ secrets.CODEFLASH_API_KEY }} + COLUMNS: 110 + MAX_RETRIES: 3 + RETRY_DELAY: 5 + EXPECTED_IMPROVEMENT_PCT: 10 + CODEFLASH_END_TO_END: 1 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Validate PR + env: + PR_AUTHOR: ${{ github.event.pull_request.user.login }} + PR_STATE: ${{ github.event.pull_request.state }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -q "^.github/workflows/"; then + echo "⚠️ Workflow changes detected." + echo "PR Author: $PR_AUTHOR" + if [[ "$PR_AUTHOR" == "misrasaurabh1" || "$PR_AUTHOR" == "KRRT7" ]]; then + echo "✅ Authorized user ($PR_AUTHOR). Proceeding." + elif [[ "$PR_STATE" == "open" ]]; then + echo "✅ PR is open. Proceeding." + else + echo "⛔ Unauthorized user ($PR_AUTHOR) attempting to modify workflows. Exiting." + exit 1 + fi + else + echo "✅ No workflow file changes detected. Proceeding." + fi + + - name: Set up JDK 11 + uses: actions/setup-java@v4 + with: + java-version: '11' + distribution: 'temurin' + cache: maven + + - name: Set up Python 3.11 for CLI + uses: astral-sh/setup-uv@v6 + with: + python-version: 3.11.6 + + - name: Install dependencies (CLI) + run: uv sync + + - name: Build codeflash-runtime JAR + run: | + cd codeflash-java-runtime + mvn clean package -q -DskipTests + mvn install -q -DskipTests + + - name: Verify Java installation + run: | + java -version + mvn --version + + - name: Run Java tracer e2e test + run: | + uv run python tests/scripts/end_to_end_test_java_tracer.py diff --git a/codeflash-java-runtime/src/main/java/com/codeflash/AgentDispatcher.java b/codeflash-java-runtime/src/main/java/com/codeflash/AgentDispatcher.java index 4eb1eef84..6c06e9ad1 100644 --- a/codeflash-java-runtime/src/main/java/com/codeflash/AgentDispatcher.java +++ b/codeflash-java-runtime/src/main/java/com/codeflash/AgentDispatcher.java @@ -19,13 +19,20 @@ */ public class AgentDispatcher { + static boolean isTracerMode(String agentArgs) { + return agentArgs != null + && (agentArgs.startsWith("trace=") || agentArgs.contains(",trace=")); + } + static boolean isProfilerMode(String agentArgs) { return agentArgs != null && (agentArgs.startsWith("config=") || agentArgs.contains(",config=")); } public static void premain(String agentArgs, Instrumentation inst) throws Exception { - if (isProfilerMode(agentArgs)) { + if (isTracerMode(agentArgs)) { + com.codeflash.tracer.TracerAgent.premain(agentArgs, inst); + } else if (isProfilerMode(agentArgs)) { com.codeflash.profiler.ProfilerAgent.premain(agentArgs, inst); } else { org.jacoco.agent.rt.internal_0e20598.PreMain.premain(agentArgs, inst); diff --git a/codeflash-java-runtime/src/main/java/com/codeflash/ReplayHelper.java b/codeflash-java-runtime/src/main/java/com/codeflash/ReplayHelper.java new file mode 100644 index 000000000..f4b9ec453 --- /dev/null +++ b/codeflash-java-runtime/src/main/java/com/codeflash/ReplayHelper.java @@ -0,0 +1,116 @@ +package com.codeflash; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.objectweb.asm.Type; + +public class ReplayHelper { + + private final Connection db; + + public ReplayHelper(String traceDbPath) { + try { + this.db = DriverManager.getConnection("jdbc:sqlite:" + traceDbPath); + } catch (SQLException e) { + throw new RuntimeException("Failed to open trace database: " + traceDbPath, e); + } + } + + public void replay(String className, String methodName, String descriptor, int invocationIndex) throws Exception { + // Query the function_calls table for this method at the given index + byte[] argsBlob; + try (PreparedStatement stmt = db.prepareStatement( + "SELECT args FROM function_calls " + + "WHERE classname = ? AND function = ? AND descriptor = ? " + + "ORDER BY time_ns LIMIT 1 OFFSET ?")) { + stmt.setString(1, className); + stmt.setString(2, methodName); + stmt.setString(3, descriptor); + stmt.setInt(4, invocationIndex); + + try (ResultSet rs = stmt.executeQuery()) { + if (!rs.next()) { + throw new RuntimeException("No invocation found at index " + invocationIndex + + " for " + className + "." + methodName + descriptor); + } + argsBlob = rs.getBytes("args"); + } + } + + // Deserialize args + Object deserialized = Serializer.deserialize(argsBlob); + if (!(deserialized instanceof Object[])) { + throw new RuntimeException("Deserialized args is not Object[], got: " + + (deserialized == null ? "null" : deserialized.getClass().getName())); + } + Object[] allArgs = (Object[]) deserialized; + + // Load the target class + Class targetClass = Class.forName(className); + + // Parse descriptor to find parameter types + Type[] paramTypes = Type.getArgumentTypes(descriptor); + Class[] paramClasses = new Class[paramTypes.length]; + for (int i = 0; i < paramTypes.length; i++) { + paramClasses[i] = typeToClass(paramTypes[i]); + } + + // Find the method + Method method = targetClass.getDeclaredMethod(methodName, paramClasses); + method.setAccessible(true); + + boolean isStatic = Modifier.isStatic(method.getModifiers()); + + if (isStatic) { + method.invoke(null, allArgs); + } else { + // Args contain only explicit parameters (no 'this'). + // Create a default instance via no-arg constructor or Kryo. + Object instance; + try { + java.lang.reflect.Constructor ctor = targetClass.getDeclaredConstructor(); + ctor.setAccessible(true); + instance = ctor.newInstance(); + } catch (NoSuchMethodException e) { + // Fall back to Objenesis instantiation (no constructor needed) + instance = new org.objenesis.ObjenesisStd().newInstance(targetClass); + } + method.invoke(instance, allArgs); + } + } + + private static Class typeToClass(Type type) throws ClassNotFoundException { + switch (type.getSort()) { + case Type.BOOLEAN: return boolean.class; + case Type.BYTE: return byte.class; + case Type.CHAR: return char.class; + case Type.SHORT: return short.class; + case Type.INT: return int.class; + case Type.LONG: return long.class; + case Type.FLOAT: return float.class; + case Type.DOUBLE: return double.class; + case Type.VOID: return void.class; + case Type.ARRAY: + Class elementClass = typeToClass(type.getElementType()); + return java.lang.reflect.Array.newInstance(elementClass, 0).getClass(); + case Type.OBJECT: + return Class.forName(type.getClassName()); + default: + throw new ClassNotFoundException("Unknown type: " + type); + } + } + + public void close() { + try { + if (db != null) db.close(); + } catch (SQLException e) { + System.err.println("Error closing ReplayHelper: " + e.getMessage()); + } + } +} diff --git a/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TraceRecorder.java b/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TraceRecorder.java new file mode 100644 index 000000000..2a22b74f4 --- /dev/null +++ b/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TraceRecorder.java @@ -0,0 +1,123 @@ +package com.codeflash.tracer; + +import com.codeflash.Serializer; + +import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; + +public final class TraceRecorder { + + private static volatile TraceRecorder instance; + + private static final long SERIALIZATION_TIMEOUT_MS = 500; + + private final TracerConfig config; + private final TraceWriter writer; + private final ConcurrentHashMap functionCounts = new ConcurrentHashMap<>(); + private final int maxFunctionCount; + private final ExecutorService serializerExecutor; + + // Reentrancy guard: prevent recursive tracing when serialization triggers class loading + private static final ThreadLocal RECORDING = ThreadLocal.withInitial(() -> Boolean.FALSE); + + private TraceRecorder(TracerConfig config) { + this.config = config; + this.writer = new TraceWriter(config.getDbPath()); + this.maxFunctionCount = config.getMaxFunctionCount(); + this.serializerExecutor = Executors.newCachedThreadPool(r -> { + Thread t = new Thread(r, "codeflash-serializer"); + t.setDaemon(true); + return t; + }); + } + + public static void initialize(TracerConfig config) { + instance = new TraceRecorder(config); + } + + public static TraceRecorder getInstance() { + return instance; + } + + public static boolean isRecording() { + return Boolean.TRUE.equals(RECORDING.get()); + } + + public void onEntry(String className, String methodName, String descriptor, + int lineNumber, String sourceFile, Object[] args) { + // Reentrancy guard + if (RECORDING.get()) { + return; + } + RECORDING.set(Boolean.TRUE); + try { + onEntryImpl(className, methodName, descriptor, lineNumber, sourceFile, args); + } finally { + RECORDING.set(Boolean.FALSE); + } + } + + private void onEntryImpl(String className, String methodName, String descriptor, + int lineNumber, String sourceFile, Object[] args) { + String qualifiedName = className + "." + methodName + descriptor; + + // Check per-method count limit + AtomicInteger count = functionCounts.computeIfAbsent(qualifiedName, k -> new AtomicInteger(0)); + if (count.get() >= maxFunctionCount) { + return; + } + + // Serialize args with timeout to prevent deep object graph traversal from blocking + byte[] argsBlob; + Future future = serializerExecutor.submit(() -> Serializer.serialize(args)); + try { + argsBlob = future.get(SERIALIZATION_TIMEOUT_MS, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + future.cancel(true); + System.err.println("[codeflash-tracer] Serialization timed out for " + className + "." + + methodName); + return; + } catch (Exception e) { + Throwable cause = e.getCause() != null ? e.getCause() : e; + System.err.println("[codeflash-tracer] Serialization failed for " + className + "." + + methodName + ": " + cause.getClass().getSimpleName() + ": " + cause.getMessage()); + return; + } + + long timeNs = System.nanoTime(); + count.incrementAndGet(); + + writer.recordFunctionCall("call", methodName, className, sourceFile, + lineNumber, descriptor, timeNs, argsBlob); + } + + public void flush() { + serializerExecutor.shutdownNow(); + // Write metadata + Map metadata = new LinkedHashMap<>(); + metadata.put("projectRoot", config.getProjectRoot()); + metadata.put("timestamp", Instant.now().toString()); + metadata.put("totalFunctions", String.valueOf(functionCounts.size())); + + int totalCaptures = 0; + for (AtomicInteger count : functionCounts.values()) { + totalCaptures += count.get(); + } + metadata.put("totalCaptures", String.valueOf(totalCaptures)); + + writer.writeMetadata(metadata); + writer.flush(); + writer.close(); + + System.err.println("[codeflash-tracer] Captured " + totalCaptures + + " invocations across " + functionCounts.size() + " methods"); + } +} diff --git a/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TraceWriter.java b/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TraceWriter.java new file mode 100644 index 000000000..a9eeabf60 --- /dev/null +++ b/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TraceWriter.java @@ -0,0 +1,210 @@ +package com.codeflash.tracer; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class TraceWriter { + + private final Connection connection; + private final BlockingQueue writeQueue; + private final Thread writerThread; + private final AtomicBoolean running; + + private PreparedStatement insertFunctionCall; + private PreparedStatement insertMetadata; + + public TraceWriter(String dbPath) { + this.writeQueue = new LinkedBlockingQueue<>(); + this.running = new AtomicBoolean(true); + + try { + Path path = Paths.get(dbPath).toAbsolutePath(); + path.getParent().toFile().mkdirs(); + this.connection = DriverManager.getConnection("jdbc:sqlite:" + path); + initializeSchema(); + prepareStatements(); + + this.writerThread = new Thread(this::writerLoop, "codeflash-trace-writer"); + this.writerThread.setDaemon(true); + this.writerThread.start(); + + } catch (SQLException e) { + throw new RuntimeException("Failed to initialize TraceWriter: " + e.getMessage(), e); + } + } + + private void initializeSchema() throws SQLException { + try (Statement stmt = connection.createStatement()) { + stmt.execute("PRAGMA journal_mode=WAL"); + stmt.execute("PRAGMA synchronous=NORMAL"); + + stmt.execute( + "CREATE TABLE IF NOT EXISTS function_calls(" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "type TEXT, " + + "function TEXT, " + + "classname TEXT, " + + "filename TEXT, " + + "line_number INTEGER, " + + "descriptor TEXT, " + + "time_ns INTEGER, " + + "args BLOB)" + ); + + stmt.execute( + "CREATE TABLE IF NOT EXISTS metadata(" + + "key TEXT PRIMARY KEY, " + + "value TEXT)" + ); + + stmt.execute("CREATE INDEX IF NOT EXISTS idx_fc_class_func ON function_calls(classname, function)"); + } + } + + private void prepareStatements() throws SQLException { + insertFunctionCall = connection.prepareStatement( + "INSERT INTO function_calls (type, function, classname, filename, line_number, descriptor, time_ns, args) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?)" + ); + insertMetadata = connection.prepareStatement( + "INSERT OR REPLACE INTO metadata (key, value) VALUES (?, ?)" + ); + } + + public void recordFunctionCall(String type, String function, String classname, + String filename, int lineNumber, String descriptor, + long timeNs, byte[] argsBlob) { + writeQueue.offer(new FunctionCallTask(type, function, classname, filename, + lineNumber, descriptor, timeNs, argsBlob)); + } + + public void writeMetadata(Map metadata) { + for (Map.Entry entry : metadata.entrySet()) { + writeQueue.offer(new MetadataTask(entry.getKey(), entry.getValue())); + } + } + + private void writerLoop() { + while (running.get() || !writeQueue.isEmpty()) { + try { + WriteTask task = writeQueue.poll(100, TimeUnit.MILLISECONDS); + if (task != null) { + task.execute(this); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } catch (SQLException e) { + System.err.println("[codeflash-tracer] Write error: " + e.getMessage()); + } + } + + // Drain remaining + WriteTask task; + while ((task = writeQueue.poll()) != null) { + try { + task.execute(this); + } catch (SQLException e) { + System.err.println("[codeflash-tracer] Write error: " + e.getMessage()); + } + } + } + + public void flush() { + while (!writeQueue.isEmpty()) { + try { + Thread.sleep(10); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + } + + public void close() { + running.set(false); + try { + writerThread.join(5000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + try { + if (insertFunctionCall != null) insertFunctionCall.close(); + if (insertMetadata != null) insertMetadata.close(); + if (connection != null) connection.close(); + } catch (SQLException e) { + System.err.println("[codeflash-tracer] Error closing TraceWriter: " + e.getMessage()); + } + } + + // Task types + + private interface WriteTask { + void execute(TraceWriter writer) throws SQLException; + } + + private static class FunctionCallTask implements WriteTask { + final String type; + final String function; + final String classname; + final String filename; + final int lineNumber; + final String descriptor; + final long timeNs; + final byte[] argsBlob; + + FunctionCallTask(String type, String function, String classname, + String filename, int lineNumber, String descriptor, + long timeNs, byte[] argsBlob) { + this.type = type; + this.function = function; + this.classname = classname; + this.filename = filename; + this.lineNumber = lineNumber; + this.descriptor = descriptor; + this.timeNs = timeNs; + this.argsBlob = argsBlob; + } + + @Override + public void execute(TraceWriter writer) throws SQLException { + writer.insertFunctionCall.setString(1, type); + writer.insertFunctionCall.setString(2, function); + writer.insertFunctionCall.setString(3, classname); + writer.insertFunctionCall.setString(4, filename); + writer.insertFunctionCall.setInt(5, lineNumber); + writer.insertFunctionCall.setString(6, descriptor); + writer.insertFunctionCall.setLong(7, timeNs); + writer.insertFunctionCall.setBytes(8, argsBlob); + writer.insertFunctionCall.executeUpdate(); + } + } + + private static class MetadataTask implements WriteTask { + final String key; + final String value; + + MetadataTask(String key, String value) { + this.key = key; + this.value = value; + } + + @Override + public void execute(TraceWriter writer) throws SQLException { + writer.insertMetadata.setString(1, key); + writer.insertMetadata.setString(2, value); + writer.insertMetadata.executeUpdate(); + } + } +} diff --git a/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracerAgent.java b/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracerAgent.java new file mode 100644 index 000000000..4aa0458fa --- /dev/null +++ b/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracerAgent.java @@ -0,0 +1,26 @@ +package com.codeflash.tracer; + +import java.lang.instrument.Instrumentation; + +public class TracerAgent { + + public static void premain(String agentArgs, Instrumentation inst) { + TracerConfig config = TracerConfig.parse(agentArgs); + + if (config.getPackages().isEmpty()) { + System.err.println("[codeflash-tracer] Warning: no packages configured, will instrument all non-JDK classes"); + } + + // Register transformer BEFORE initializing TraceRecorder, to ensure + // classes loaded during initialization (SQLite, Kryo) are visible. + inst.addTransformer(new TracingTransformer(config), true); + + TraceRecorder.initialize(config); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + TraceRecorder.getInstance().flush(); + }, "codeflash-tracer-shutdown")); + + System.err.println("[codeflash-tracer] Agent loaded, tracing packages: " + config.getPackages()); + } +} diff --git a/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracerConfig.java b/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracerConfig.java new file mode 100644 index 000000000..8fe799d2f --- /dev/null +++ b/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracerConfig.java @@ -0,0 +1,113 @@ +package com.codeflash.tracer; + +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; + +public final class TracerConfig { + + @SerializedName("dbPath") + private String dbPath = "codeflash_trace.db"; + + @SerializedName("packages") + private List packages = Collections.emptyList(); + + @SerializedName("excludePackages") + private List excludePackages = Collections.emptyList(); + + @SerializedName("maxFunctionCount") + private int maxFunctionCount = 256; + + @SerializedName("timeout") + private int timeout = 0; + + @SerializedName("projectRoot") + private String projectRoot = ""; + + private static final Gson GSON = new Gson(); + + public static TracerConfig parse(String agentArgs) { + if (agentArgs == null || agentArgs.isEmpty()) { + return new TracerConfig(); + } + + String configPath = null; + for (String part : agentArgs.split(",")) { + String trimmed = part.trim(); + if (trimmed.startsWith("trace=")) { + configPath = trimmed.substring("trace=".length()); + } + } + + if (configPath == null) { + System.err.println("[codeflash-tracer] No trace= in agent args: " + agentArgs); + return new TracerConfig(); + } + + try { + String json = new String(Files.readAllBytes(Paths.get(configPath)), StandardCharsets.UTF_8); + TracerConfig config = GSON.fromJson(json, TracerConfig.class); + if (config == null) { + return new TracerConfig(); + } + if (config.packages == null) config.packages = Collections.emptyList(); + if (config.excludePackages == null) config.excludePackages = Collections.emptyList(); + return config; + } catch (IOException e) { + System.err.println("[codeflash-tracer] Failed to read config: " + e.getMessage()); + return new TracerConfig(); + } + } + + public String getDbPath() { + return dbPath; + } + + public List getPackages() { + return packages; + } + + public List getExcludePackages() { + return excludePackages; + } + + public int getMaxFunctionCount() { + return maxFunctionCount; + } + + public int getTimeout() { + return timeout; + } + + public String getProjectRoot() { + return projectRoot; + } + + public boolean shouldInstrumentClass(String internalClassName) { + String dotName = internalClassName.replace('/', '.'); + + for (String excluded : excludePackages) { + if (dotName.startsWith(excluded)) { + return false; + } + } + + if (packages.isEmpty()) { + return true; + } + + for (String pkg : packages) { + if (dotName.startsWith(pkg)) { + return true; + } + } + + return false; + } +} diff --git a/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingClassVisitor.java b/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingClassVisitor.java new file mode 100644 index 000000000..c760ea636 --- /dev/null +++ b/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingClassVisitor.java @@ -0,0 +1,43 @@ +package com.codeflash.tracer; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +public class TracingClassVisitor extends ClassVisitor { + + private final String internalClassName; + private String sourceFile; + + public TracingClassVisitor(ClassVisitor classVisitor, String internalClassName) { + super(Opcodes.ASM9, classVisitor); + this.internalClassName = internalClassName; + } + + @Override + public void visitSource(String source, String debug) { + super.visitSource(source, debug); + this.sourceFile = source; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, + String signature, String[] exceptions) { + MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); + + // Skip static initializers, synthetic, and bridge methods + if (name.equals("") + || (access & Opcodes.ACC_SYNTHETIC) != 0 + || (access & Opcodes.ACC_BRIDGE) != 0) { + return mv; + } + + // Skip constructors for now (they have complex init semantics) + if (name.equals("")) { + return mv; + } + + return new TracingMethodAdapter(mv, access, name, descriptor, + internalClassName, 0, sourceFile != null ? sourceFile : ""); + } +} diff --git a/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingMethodAdapter.java b/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingMethodAdapter.java new file mode 100644 index 000000000..de71d4984 --- /dev/null +++ b/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingMethodAdapter.java @@ -0,0 +1,132 @@ +package com.codeflash.tracer; + +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.AdviceAdapter; + +/** + * ASM AdviceAdapter that captures method arguments on entry. + * + *

On method entry, boxes all parameters into an Object[] array and calls + * {@link TraceRecorder#onEntry} to record the invocation. For instance methods, + * {@code this} is included as the first element. + */ +public class TracingMethodAdapter extends AdviceAdapter { + + private static final String TRACE_RECORDER = "com/codeflash/tracer/TraceRecorder"; + + private final String className; + private final String methodName; + private final String descriptor; + private final int lineNumber; + private final String sourceFile; + private final boolean isStatic; + + protected TracingMethodAdapter(MethodVisitor mv, int access, String name, String descriptor, + String className, int lineNumber, String sourceFile) { + super(Opcodes.ASM9, mv, access, name, descriptor); + this.className = className; + this.methodName = name; + this.descriptor = descriptor; + this.lineNumber = lineNumber; + this.sourceFile = sourceFile; + this.isStatic = (access & Opcodes.ACC_STATIC) != 0; + } + + @Override + protected void onMethodEnter() { + // Build Object[] containing explicit parameters only (skip 'this' to avoid + // expensive serialization of the receiver's full object graph) + Type[] argTypes = Type.getArgumentTypes(descriptor); + + // Push array size and create Object[] + pushInt(argTypes.length); + mv.visitTypeInsn(ANEWARRAY, "java/lang/Object"); + + int arrayIndex = 0; + int localIndex = isStatic ? 0 : 1; // skip 'this' slot for instance methods + + // Box and store each parameter + for (Type argType : argTypes) { + mv.visitInsn(DUP); + pushInt(arrayIndex); + loadAndBox(argType, localIndex); + mv.visitInsn(AASTORE); + arrayIndex++; + localIndex += argType.getSize(); + } + + // Stack now has: Object[] args on top + // Store in a local variable + int argsLocal = newLocal(Type.getType("[Ljava/lang/Object;")); + mv.visitVarInsn(ASTORE, argsLocal); + + // Call TraceRecorder.getInstance().onEntry(className, methodName, descriptor, lineNumber, sourceFile, args) + mv.visitMethodInsn(INVOKESTATIC, TRACE_RECORDER, "getInstance", + "()L" + TRACE_RECORDER + ";", false); + mv.visitLdcInsn(className.replace('/', '.')); + mv.visitLdcInsn(methodName); + mv.visitLdcInsn(descriptor); + pushInt(lineNumber); + mv.visitLdcInsn(sourceFile != null ? sourceFile : ""); + mv.visitVarInsn(ALOAD, argsLocal); + mv.visitMethodInsn(INVOKEVIRTUAL, TRACE_RECORDER, "onEntry", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;[Ljava/lang/Object;)V", + false); + } + + private void loadAndBox(Type type, int localIndex) { + switch (type.getSort()) { + case Type.BOOLEAN: + mv.visitVarInsn(ILOAD, localIndex); + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;", false); + break; + case Type.BYTE: + mv.visitVarInsn(ILOAD, localIndex); + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Byte", "valueOf", "(B)Ljava/lang/Byte;", false); + break; + case Type.CHAR: + mv.visitVarInsn(ILOAD, localIndex); + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Character", "valueOf", "(C)Ljava/lang/Character;", false); + break; + case Type.SHORT: + mv.visitVarInsn(ILOAD, localIndex); + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Short", "valueOf", "(S)Ljava/lang/Short;", false); + break; + case Type.INT: + mv.visitVarInsn(ILOAD, localIndex); + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false); + break; + case Type.LONG: + mv.visitVarInsn(LLOAD, localIndex); + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false); + break; + case Type.FLOAT: + mv.visitVarInsn(FLOAD, localIndex); + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Float", "valueOf", "(F)Ljava/lang/Float;", false); + break; + case Type.DOUBLE: + mv.visitVarInsn(DLOAD, localIndex); + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Double", "valueOf", "(D)Ljava/lang/Double;", false); + break; + default: + // Object or array — just load reference + mv.visitVarInsn(ALOAD, localIndex); + break; + } + } + + private void pushInt(int value) { + if (value >= -1 && value <= 5) { + mv.visitInsn(ICONST_0 + value); + } else if (value >= Byte.MIN_VALUE && value <= Byte.MAX_VALUE) { + mv.visitIntInsn(BIPUSH, value); + } else if (value >= Short.MIN_VALUE && value <= Short.MAX_VALUE) { + mv.visitIntInsn(SIPUSH, value); + } else { + mv.visitLdcInsn(value); + } + } +} diff --git a/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingTransformer.java b/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingTransformer.java new file mode 100644 index 000000000..974c767a9 --- /dev/null +++ b/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingTransformer.java @@ -0,0 +1,65 @@ +package com.codeflash.tracer; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; + +import java.lang.instrument.ClassFileTransformer; +import java.security.ProtectionDomain; + +public class TracingTransformer implements ClassFileTransformer { + + private final TracerConfig config; + + public TracingTransformer(TracerConfig config) { + this.config = config; + } + + @Override + public byte[] transform(ClassLoader loader, String className, + Class classBeingRedefined, ProtectionDomain protectionDomain, + byte[] classfileBuffer) { + if (className == null || !config.shouldInstrumentClass(className)) { + return null; + } + + // Skip instrumentation if we're inside a recording call (e.g., during Kryo serialization) + if (TraceRecorder.isRecording()) { + return null; + } + + // Skip internal JDK, framework, and synthetic classes + if (className.startsWith("java/") + || className.startsWith("javax/") + || className.startsWith("jdk/") + || className.startsWith("sun/") + || className.startsWith("com/sun/") + || className.startsWith("com/codeflash/") + || className.contains("ConstructorAccess") + || className.contains("FieldAccess") + || className.contains("$$")) { + return null; + } + + try { + return instrumentClass(className, classfileBuffer); + } catch (Throwable e) { + System.err.println("[codeflash-tracer] Failed to instrument " + className + ": " + + e.getClass().getName() + ": " + e.getMessage()); + return null; + } + } + + private byte[] instrumentClass(String internalClassName, byte[] bytecode) { + ClassReader cr = new ClassReader(bytecode); + // Use COMPUTE_MAXS only (not COMPUTE_FRAMES) to preserve original stack map frames. + // COMPUTE_FRAMES recomputes all frames and calls getCommonSuperClass() which either + // triggers classloader deadlocks or produces incorrect frames when returning "java/lang/Object". + // With COMPUTE_MAXS + ClassReader passed to constructor, ASM copies original frames and + // adjusts offsets for injected code. Our AdviceAdapter only injects at method entry + // (before any branch points), so existing frames remain valid. + ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS); + TracingClassVisitor cv = new TracingClassVisitor(cw, internalClassName); + cr.accept(cv, ClassReader.EXPAND_FRAMES); + return cw.toByteArray(); + } +} diff --git a/codeflash/benchmarking/function_ranker.py b/codeflash/benchmarking/function_ranker.py index 20c45f443..7337c3c4d 100644 --- a/codeflash/benchmarking/function_ranker.py +++ b/codeflash/benchmarking/function_ranker.py @@ -11,6 +11,7 @@ from pathlib import Path from codeflash.discovery.functions_to_optimize import FunctionToOptimize + from codeflash.languages.java.jfr_parser import JfrProfile pytest_patterns = { " bool: """Check if a function is part of pytest infrastructure that should be excluded from ranking. @@ -38,6 +52,90 @@ def is_pytest_infrastructure(filename: str, function_name: str) -> bool: return any(pattern in function_name.lower() for pattern in pytest_func_patterns) +def is_java_infrastructure(class_name: str) -> bool: + return any(class_name.startswith(pattern) for pattern in java_infra_patterns) + + +class JavaFunctionRanker: + """Ranks Java functions using JFR profiling data.""" + + def __init__(self, jfr_profile: JfrProfile) -> None: + self._jfr_profile = jfr_profile + self._ranking = jfr_profile.get_method_ranking() + + def get_function_stats_summary(self, function_to_optimize: FunctionToOptimize) -> dict | None: + for entry in self._ranking: + if entry["method_name"] == function_to_optimize.function_name: + return { + "filename": "", + "function_name": entry["method_name"], + "qualified_name": f"{entry['class_name']}.{entry['method_name']}", + "class_name": entry["class_name"], + "line_number": 0, + "call_count": entry["sample_count"], + "own_time_ns": self._jfr_profile.get_addressable_time_ns(entry["class_name"], entry["method_name"]), + "addressable_time_ns": self._jfr_profile.get_addressable_time_ns( + entry["class_name"], entry["method_name"] + ), + } + return None + + def get_function_addressable_time(self, function_to_optimize: FunctionToOptimize) -> float: + stats = self.get_function_stats_summary(function_to_optimize) + return stats["addressable_time_ns"] if stats else 0.0 + + def rank_functions( + self, functions_to_optimize: list[FunctionToOptimize], min_functions: int = 5 + ) -> list[FunctionToOptimize]: + if not self._ranking: + logger.warning("No JFR profiling data available to rank functions.") + return functions_to_optimize + + total_time = sum( + self._jfr_profile.get_addressable_time_ns(e["class_name"], e["method_name"]) + for e in self._ranking + if not is_java_infrastructure(e["class_name"]) + ) + + if total_time == 0: + return functions_to_optimize + + functions_with_time = [] + functions_without_time = [] + for func in functions_to_optimize: + addr_time = self.get_function_addressable_time(func) + if addr_time > 0: + importance = addr_time / total_time + if importance >= DEFAULT_IMPORTANCE_THRESHOLD: + functions_with_time.append(func) + else: + logger.debug( + f"Filtering out Java function {func.qualified_name} with importance " + f"{importance:.2%} (below threshold {DEFAULT_IMPORTANCE_THRESHOLD:.2%})" + ) + functions_without_time.append(func) + else: + functions_without_time.append(func) + + ranked = sorted(functions_with_time, key=self.get_function_addressable_time, reverse=True) + + # Guarantee at least min_functions pass through even when JFR data is sparse. + # Functions without JFR samples may still benefit from optimization. + if len(ranked) < min_functions: + shortfall = min_functions - len(ranked) + ranked_set = {id(f) for f in ranked} + for func in functions_without_time[:shortfall]: + if id(func) not in ranked_set: + ranked.append(func) + if shortfall > 0: + logger.info( + f"JFR data only covered {len(functions_with_time)} functions; " + f"added {min(shortfall, len(functions_without_time))} more to meet minimum of {min_functions}" + ) + + return ranked + + class FunctionRanker: """Ranks and filters functions based on % of addressable time derived from profiling data. diff --git a/codeflash/discovery/discover_unit_tests.py b/codeflash/discovery/discover_unit_tests.py index ba3371a90..5283f31ac 100644 --- a/codeflash/discovery/discover_unit_tests.py +++ b/codeflash/discovery/discover_unit_tests.py @@ -648,26 +648,32 @@ def discover_tests_for_language( # Convert TestInfo back to FunctionCalledInTest format # Use the full qualified name (with modules) as the key for consistency with Python function_to_tests: dict[str, set[FunctionCalledInTest]] = defaultdict(set) - num_tests = 0 + num_unit_tests = 0 + num_replay_tests = 0 for qualified_name, test_infos in test_map.items(): # Convert simple qualified_name to full qualified_name_with_modules full_qualified_name = simple_to_full_name.get(qualified_name, qualified_name) for test_info in test_infos: + is_replay = getattr(test_info, "is_replay", False) + test_type = TestType.REPLAY_TEST if is_replay else TestType.EXISTING_UNIT_TEST function_to_tests[full_qualified_name].add( FunctionCalledInTest( tests_in_file=TestsInFile( test_file=test_info.test_file, test_class=test_info.test_class, test_function=test_info.test_name, - test_type=TestType.EXISTING_UNIT_TEST, + test_type=test_type, ), position=CodePosition(line_no=0, col_no=0), ) ) - num_tests += 1 + if is_replay: + num_replay_tests += 1 + else: + num_unit_tests += 1 - return dict(function_to_tests), num_tests, 0 + return dict(function_to_tests), num_unit_tests, num_replay_tests def discover_unit_tests( diff --git a/codeflash/discovery/functions_to_optimize.py b/codeflash/discovery/functions_to_optimize.py index 9c8776f75..607039273 100644 --- a/codeflash/discovery/functions_to_optimize.py +++ b/codeflash/discovery/functions_to_optimize.py @@ -465,6 +465,10 @@ def find_all_functions_in_file(file_path: Path) -> dict[Path, list[FunctionToOpt def get_all_replay_test_functions( replay_test: list[Path], test_cfg: TestConfig, project_root_path: Path ) -> tuple[dict[Path, list[FunctionToOptimize]], Path]: + # Check if these are Java replay tests + if replay_test and replay_test[0].suffix == ".java": + return _get_java_replay_test_functions(replay_test, test_cfg, project_root_path) + trace_file_path: Path | None = None for replay_test_file in replay_test: try: @@ -549,6 +553,75 @@ def get_all_replay_test_functions( return dict(filtered_valid_functions), trace_file_path +def _get_java_replay_test_functions( + replay_test: list[Path], test_cfg: TestConfig, project_root_path: Path +) -> tuple[dict[Path, list[FunctionToOptimize]], Path]: + """Parse Java replay test files to extract functions and trace file path.""" + from codeflash.languages.java.replay_test import parse_replay_test_metadata + + trace_file_path: Path | None = None + functions: dict[Path, list[FunctionToOptimize]] = defaultdict(list) + + for test_file in replay_test: + metadata = parse_replay_test_metadata(test_file) + + if trace_file_path is None and "trace_file" in metadata: + trace_file_path = Path(metadata["trace_file"]) + + classname = metadata.get("classname", "") + function_names = metadata.get("functions", "").split(",") + + if not classname or not function_names: + continue + + # Resolve the source file from the classname (e.g., "com.aerospike.benchmarks.Main") + class_parts = classname.split(".") + # Try matching by full package path first (e.g., com/aerospike/benchmarks/Main.java) + expected_path_suffix = "/".join(class_parts) + ".java" + source_file = None + for java_file in project_root_path.rglob("*.java"): + if java_file.as_posix().endswith(expected_path_suffix): + source_file = java_file + break + # Fall back to simple name match + if source_file is None: + for java_file in project_root_path.rglob("*.java"): + if java_file.stem == class_parts[-1]: + source_file = java_file + break + + if source_file is None: + logger.warning(f"Could not find source file for class {classname}") + continue + + # Use Java discovery to find functions in the source file + from codeflash.languages.registry import get_language_support + + lang_support = get_language_support(source_file) + source_code = source_file.read_text(encoding="utf-8") + all_functions = lang_support.discover_functions(source_code, source_file) + + for func in all_functions: + if func.function_name in function_names: + functions[source_file].append(func) + + if trace_file_path is None: + logger.error("Could not find trace_file_path in Java replay test files.") + from codeflash.code_utils.code_utils import exit_with_message + + exit_with_message("Could not find trace_file_path in Java replay test files.") + raise AssertionError("Unreachable") + + if not trace_file_path.exists(): + from codeflash.code_utils.code_utils import exit_with_message + + exit_with_message( + f"Trace file not found: {trace_file_path}\nPlease regenerate the replay test by re-running the tracer." + ) + + return dict(functions), trace_file_path + + def is_git_repo(file_path: str) -> bool: try: git.Repo(file_path, search_parent_directories=True) diff --git a/codeflash/languages/base.py b/codeflash/languages/base.py index f8f890163..bcdabeb8d 100644 --- a/codeflash/languages/base.py +++ b/codeflash/languages/base.py @@ -115,6 +115,7 @@ class TestInfo: test_name: str test_file: Path test_class: str | None = None + is_replay: bool = False @property def full_test_path(self) -> str: diff --git a/codeflash/languages/java/jfr_parser.py b/codeflash/languages/java/jfr_parser.py new file mode 100644 index 000000000..c44e8f46b --- /dev/null +++ b/codeflash/languages/java/jfr_parser.py @@ -0,0 +1,180 @@ +from __future__ import annotations + +import json +import logging +import shutil +import subprocess +from pathlib import Path + +logger = logging.getLogger(__name__) + + +class JfrProfile: + """Parses JFR (Java Flight Recorder) files for method-level profiling data. + + Uses the `jfr` CLI tool (ships with JDK 11+) to extract ExecutionSample events + and build method-level timing estimates from sampling data. + """ + + def __init__(self, jfr_file: Path, packages: list[str]) -> None: + self.jfr_file = jfr_file + self.packages = packages + self._method_samples: dict[str, int] = {} + self._method_info: dict[str, dict[str, str]] = {} + self._caller_map: dict[str, dict[str, int]] = {} + self._recording_duration_ns: int = 0 + self._total_samples: int = 0 + self._parse() + + def _find_jfr_tool(self) -> str | None: + jfr_path = shutil.which("jfr") + if jfr_path: + return jfr_path + + java_home = subprocess.run( + ["java", "-XshowSettings:property", "-version"], capture_output=True, text=True, check=False + ) + for line in java_home.stderr.splitlines(): + if "java.home" in line: + home = line.split("=", 1)[1].strip() + candidate = Path(home) / "bin" / "jfr" + if candidate.exists(): + return str(candidate) + return None + + def _parse(self) -> None: + if not self.jfr_file.exists(): + logger.warning("JFR file not found: %s", self.jfr_file) + return + + jfr_tool = self._find_jfr_tool() + if jfr_tool is None: + logger.warning("jfr CLI tool not found, cannot parse JFR profile") + return + + try: + result = subprocess.run( + [jfr_tool, "print", "--events", "jdk.ExecutionSample", "--json", str(self.jfr_file)], + capture_output=True, + text=True, + timeout=120, + check=False, + ) + if result.returncode != 0: + logger.warning("jfr print failed: %s", result.stderr) + return + self._parse_json(result.stdout) + except subprocess.TimeoutExpired: + logger.warning("jfr print timed out for %s", self.jfr_file) + except Exception: + logger.exception("Failed to parse JFR file %s", self.jfr_file) + + def _parse_json(self, json_str: str) -> None: + try: + data = json.loads(json_str) + except json.JSONDecodeError: + logger.warning("Failed to parse JFR JSON output") + return + + events = data.get("recording", {}).get("events", []) + if not events: + events = data.get("events", []) + + for event in events: + if event.get("type") != "jdk.ExecutionSample": + continue + + stack_trace = event.get("values", {}).get("stackTrace", {}) + frames = stack_trace.get("frames", []) + if not frames: + continue + + self._total_samples += 1 + + # Top-of-stack = own time + top_frame = frames[0] + top_method_key = self._frame_to_key(top_frame) + if top_method_key and self._matches_packages(top_method_key): + self._method_samples[top_method_key] = self._method_samples.get(top_method_key, 0) + 1 + self._store_method_info(top_method_key, top_frame) + + # Build caller-callee relationships from adjacent frames + for i in range(len(frames) - 1): + callee_key = self._frame_to_key(frames[i]) + caller_key = self._frame_to_key(frames[i + 1]) + if callee_key and caller_key and self._matches_packages(callee_key): + callee_callers = self._caller_map.setdefault(callee_key, {}) + callee_callers[caller_key] = callee_callers.get(caller_key, 0) + 1 + + # Estimate recording duration from event timestamps + if events: + timestamps = [] + for event in events: + start_time = event.get("values", {}).get("startTime") + if start_time: + try: + # JFR timestamps are in ISO format or epoch nanos + if isinstance(start_time, str): + from datetime import datetime + + dt = datetime.fromisoformat(start_time.replace("Z", "+00:00")) + timestamps.append(int(dt.timestamp() * 1_000_000_000)) + elif isinstance(start_time, (int, float)): + timestamps.append(int(start_time)) + except (ValueError, TypeError): + continue + if len(timestamps) >= 2: + self._recording_duration_ns = max(timestamps) - min(timestamps) + + def _frame_to_key(self, frame: dict) -> str | None: + method = frame.get("method", {}) + class_name = method.get("type", {}).get("name", "") + method_name = method.get("name", "") + if not class_name or not method_name: + return None + return f"{class_name}.{method_name}" + + def _store_method_info(self, key: str, frame: dict) -> None: + if key in self._method_info: + return + method = frame.get("method", {}) + self._method_info[key] = { + "class_name": method.get("type", {}).get("name", ""), + "method_name": method.get("name", ""), + "descriptor": method.get("descriptor", ""), + "line_number": str(frame.get("lineNumber", 0)), + } + + def _matches_packages(self, method_key: str) -> bool: + if not self.packages: + return True + return any(method_key.startswith(pkg) for pkg in self.packages) + + def get_method_ranking(self) -> list[dict]: + if not self._method_samples or self._total_samples == 0: + return [] + + ranking = [] + for method_key, sample_count in sorted(self._method_samples.items(), key=lambda x: x[1], reverse=True): + info = self._method_info.get(method_key, {}) + ranking.append( + { + "class_name": info.get("class_name", method_key.rsplit(".", 1)[0]), + "method_name": info.get("method_name", method_key.rsplit(".", 1)[-1]), + "sample_count": sample_count, + "pct_of_total": (sample_count / self._total_samples) * 100, + } + ) + return ranking + + def get_addressable_time_ns(self, class_name: str, method_name: str) -> float: + method_key = f"{class_name}.{method_name}" + sample_count = self._method_samples.get(method_key, 0) + if sample_count == 0 or self._total_samples == 0: + return 0.0 + + if self._recording_duration_ns > 0: + return (sample_count / self._total_samples) * self._recording_duration_ns + + # Fallback: return sample count as a proxy (higher = more time) + return float(sample_count * 1_000_000) diff --git a/codeflash/languages/java/replay_test.py b/codeflash/languages/java/replay_test.py new file mode 100644 index 000000000..c753bf4fa --- /dev/null +++ b/codeflash/languages/java/replay_test.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +import logging +import re +import sqlite3 +from collections import defaultdict +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pathlib import Path + +logger = logging.getLogger(__name__) + + +def generate_replay_tests(trace_db_path: Path, output_dir: Path, project_root: Path, max_run_count: int = 256) -> int: + """Generate JUnit 5 replay test files from a trace SQLite database. + + Returns the number of test files generated. + """ + if not trace_db_path.exists(): + logger.error("Trace database not found: %s", trace_db_path) + return 0 + + output_dir.mkdir(parents=True, exist_ok=True) + + conn = sqlite3.connect(str(trace_db_path)) + try: + cursor = conn.execute( + "SELECT DISTINCT classname, function, descriptor FROM function_calls ORDER BY classname, function" + ) + methods = cursor.fetchall() + + # Group by class + class_methods: dict[str, list[tuple[str, str]]] = defaultdict(list) + for classname, function, descriptor in methods: + class_methods[classname].append((function, descriptor)) + + test_count = 0 + all_function_names: list[str] = [] + + for classname, method_list in class_methods.items(): + safe_class_name = _sanitize_identifier(classname.replace(".", "_")) + test_class_name = f"ReplayTest_{safe_class_name}" + + test_methods_code: list[str] = [] + class_function_names: list[str] = [] + + for method_name, descriptor in method_list: + # Count invocations for this method + count_result = conn.execute( + "SELECT COUNT(*) FROM function_calls WHERE classname = ? AND function = ? AND descriptor = ?", + (classname, method_name, descriptor), + ).fetchone() + invocation_count = min(count_result[0], max_run_count) + + class_function_names.append(method_name) + safe_method = _sanitize_identifier(method_name) + + for i in range(invocation_count): + escaped_descriptor = descriptor.replace('"', '\\"') + test_methods_code.append( + f" @Test void replay_{safe_method}_{i}() throws Exception {{\n" + f' helper.replay("{classname}", "{method_name}", ' + f'"{escaped_descriptor}", {i});\n' + f" }}" + ) + + all_function_names.extend(class_function_names) + + # Generate the test file + functions_comment = ",".join(class_function_names) + test_content = ( + f"// codeflash:functions={functions_comment}\n" + f"// codeflash:trace_file={trace_db_path.as_posix()}\n" + f"// codeflash:classname={classname}\n" + f"package codeflash.replay;\n\n" + f"import org.junit.jupiter.api.Test;\n" + f"import org.junit.jupiter.api.AfterAll;\n" + f"import com.codeflash.ReplayHelper;\n\n" + f"class {test_class_name} {{\n" + f" private static final ReplayHelper helper =\n" + f' new ReplayHelper("{trace_db_path.as_posix()}");\n\n' + f" @AfterAll static void cleanup() {{ helper.close(); }}\n\n" + "\n\n".join(test_methods_code) + "\n" + "}\n" + ) + + test_file = output_dir / f"{test_class_name}.java" + test_file.write_text(test_content, encoding="utf-8") + test_count += 1 + logger.info("Generated replay test: %s (%d test methods)", test_file.name, len(test_methods_code)) + + finally: + conn.close() + + return test_count + + +def _sanitize_identifier(name: str) -> str: + """Sanitize a string for use as a Java identifier.""" + return re.sub(r"[^a-zA-Z0-9_]", "_", name) + + +def parse_replay_test_metadata(test_file: Path) -> dict[str, str]: + """Parse codeflash metadata comments from a Java replay test file. + + Returns a dict with keys: functions, trace_file, classname. + """ + metadata: dict[str, str] = {} + try: + with test_file.open("r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line.startswith("// codeflash:"): + if line and not line.startswith("//"): + break + continue + key_value = line[len("// codeflash:") :] + if "=" in key_value: + key, value = key_value.split("=", 1) + metadata[key] = value + except Exception: + logger.exception("Failed to parse replay test metadata from %s", test_file) + return metadata diff --git a/codeflash/languages/java/resources/codeflash-runtime-1.0.0.jar b/codeflash/languages/java/resources/codeflash-runtime-1.0.0.jar index 842f2c19bb8c32f6a55e868a6e35de0caa30a432..cfcee9390d6529e7aa1d8639566228ce132a72d0 100644 GIT binary patch delta 71243 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

n>&XW@q;t->7-G&Tq*_r|Wvl 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 dict[str, str] | None: + """Check if a test file is a replay test and extract its metadata. + + Returns metadata dict if it's a replay test, None otherwise. + """ + try: + metadata: dict[str, str] = {} + with test_file.open("r", encoding="utf-8") as f: + for line in f: + stripped = line.strip() + if not stripped.startswith("// codeflash:"): + if stripped and not stripped.startswith("//"): + break + continue + key_value = stripped[len("// codeflash:") :] + if "=" in key_value: + key, value = key_value.split("=", 1) + metadata[key] = value + return metadata if "functions" in metadata else None + except Exception: + return None + + +def _discover_replay_tests( + test_file: Path, + metadata: dict[str, str], + function_map: dict[str, FunctionToOptimize], + result: dict[str, list[TestInfo]], + analyzer: JavaAnalyzer, +) -> None: + """Map replay test methods to source functions using metadata comments.""" + function_names = [f.strip() for f in metadata.get("functions", "").split(",") if f.strip()] + test_methods = discover_test_methods(test_file, analyzer) + + # Extract test class name from the file + test_class = None + if test_methods: + test_class = test_methods[0].class_name + + for test_method in test_methods: + # Each replay test method is named replay__ + # Map it to the source function it exercises + for func_name in function_names: + if func_name in function_map: + qualified_name = function_map[func_name].qualified_name + result[qualified_name].append( + TestInfo( + test_name=test_method.function_name, + test_file=test_file, + test_class=test_class or test_method.class_name, + is_replay=True, + ) + ) + + logger.debug( + "Discovered %d replay test methods for functions %s in %s", len(test_methods), function_names, test_file.name + ) + + def _compute_file_context(test_source: str, analyzer: JavaAnalyzer) -> tuple: """Pre-compute per-file analysis data: parse tree and static imports. diff --git a/codeflash/languages/java/tracer.py b/codeflash/languages/java/tracer.py new file mode 100644 index 000000000..94127b884 --- /dev/null +++ b/codeflash/languages/java/tracer.py @@ -0,0 +1,167 @@ +from __future__ import annotations + +import json +import logging +import os +import subprocess +from typing import TYPE_CHECKING + +from codeflash.languages.java.line_profiler import find_agent_jar +from codeflash.languages.java.replay_test import generate_replay_tests + +if TYPE_CHECKING: + from pathlib import Path + +logger = logging.getLogger(__name__) + +# --add-opens flags needed for Kryo serialization on Java 16+ +ADD_OPENS_FLAGS = ( + "--add-opens=java.base/java.util=ALL-UNNAMED " + "--add-opens=java.base/java.lang=ALL-UNNAMED " + "--add-opens=java.base/java.lang.reflect=ALL-UNNAMED " + "--add-opens=java.base/java.math=ALL-UNNAMED " + "--add-opens=java.base/java.io=ALL-UNNAMED " + "--add-opens=java.base/java.net=ALL-UNNAMED " + "--add-opens=java.base/java.time=ALL-UNNAMED" +) + + +class JavaTracer: + """Orchestrates two-stage Java tracing: JFR profiling + argument capture.""" + + def trace( + self, + java_command: list[str], + trace_db_path: Path, + packages: list[str], + project_root: Path | None = None, + max_function_count: int = 256, + timeout: int = 0, + ) -> tuple[Path, Path]: + """Run the Java program twice: once for profiling, once for arg capture. + + Returns (trace_db_path, jfr_file_path). + """ + jfr_file = trace_db_path.with_suffix(".jfr") + trace_db_path.parent.mkdir(parents=True, exist_ok=True) + + # Stage 1: JFR Profiling + logger.info("Stage 1: Running JFR profiling...") + jfr_env = self.build_jfr_env(jfr_file) + try: + subprocess.run(java_command, env=jfr_env, check=False, timeout=timeout or None) + except subprocess.TimeoutExpired: + logger.warning("JFR profiling stage timed out after %d seconds", timeout) + + if not jfr_file.exists(): + logger.warning("JFR file was not created at %s", jfr_file) + + # Stage 2: Argument Capture via Tracing Agent + logger.info("Stage 2: Running argument capture...") + config_path = self.create_tracer_config( + trace_db_path, packages, project_root=project_root, max_function_count=max_function_count, timeout=timeout + ) + agent_env = self.build_agent_env(config_path) + try: + subprocess.run(java_command, env=agent_env, check=False, timeout=timeout or None) + except subprocess.TimeoutExpired: + logger.warning("Argument capture stage timed out after %d seconds", timeout) + + if not trace_db_path.exists(): + logger.error("Trace database was not created at %s", trace_db_path) + + return trace_db_path, jfr_file + + def create_tracer_config( + self, + trace_db_path: Path, + packages: list[str], + project_root: Path | None = None, + max_function_count: int = 256, + timeout: int = 0, + ) -> Path: + config = { + "dbPath": str(trace_db_path.resolve()), + "packages": packages, + "excludePackages": [], + "maxFunctionCount": max_function_count, + "timeout": timeout, + "projectRoot": str(project_root.resolve()) if project_root else "", + } + + config_path = trace_db_path.with_suffix(".config.json") + config_path.write_text(json.dumps(config, indent=2), encoding="utf-8") + return config_path + + def build_jfr_env(self, jfr_file: Path) -> dict[str, str]: + env = os.environ.copy() + jfr_opts = f"-XX:StartFlightRecording=filename={jfr_file.resolve()},settings=profile,dumponexit=true" + existing = env.get("JAVA_TOOL_OPTIONS", "") + env["JAVA_TOOL_OPTIONS"] = f"{existing} {jfr_opts}".strip() + return env + + def build_agent_env(self, config_path: Path) -> dict[str, str]: + env = os.environ.copy() + agent_jar = find_agent_jar() + if agent_jar is None: + msg = "codeflash-runtime JAR not found, cannot run tracing agent" + raise FileNotFoundError(msg) + + agent_opts = f"{ADD_OPENS_FLAGS} -javaagent:{agent_jar}=trace={config_path.resolve()}" + existing = env.get("JAVA_TOOL_OPTIONS", "") + env["JAVA_TOOL_OPTIONS"] = f"{existing} {agent_opts}".strip() + return env + + @staticmethod + def detect_packages_from_source(module_root: Path) -> list[str]: + """Scan Java files for package declarations and return unique package prefixes.""" + packages: set[str] = set() + for java_file in module_root.rglob("*.java"): + try: + with java_file.open("r", encoding="utf-8") as f: + for line in f: + stripped = line.strip() + if stripped.startswith("package "): + pkg = stripped[8:].rstrip(";").strip() + # Use top two levels as prefix (e.g., "com.aerospike") + parts = pkg.split(".") + prefix = ".".join(parts[: min(2, len(parts))]) + packages.add(prefix) + break + if stripped and not stripped.startswith("//") and not stripped.startswith("/*"): + break + except (OSError, UnicodeDecodeError): + continue + + return sorted(packages) + + +def run_java_tracer( + java_command: list[str], + trace_db_path: Path, + packages: list[str], + project_root: Path, + output_dir: Path, + max_function_count: int = 256, + timeout: int = 0, + max_run_count: int = 256, +) -> tuple[Path, Path, int]: + """High-level entry point: trace a Java command and generate replay tests. + + Returns (trace_db_path, jfr_file, test_count). + """ + tracer = JavaTracer() + trace_db, jfr_file = tracer.trace( + java_command=java_command, + trace_db_path=trace_db_path, + packages=packages, + project_root=project_root, + max_function_count=max_function_count, + timeout=timeout, + ) + + test_count = generate_replay_tests( + trace_db_path=trace_db, output_dir=output_dir, project_root=project_root, max_run_count=max_run_count + ) + + return trace_db, jfr_file, test_count diff --git a/codeflash/optimization/optimizer.py b/codeflash/optimization/optimizer.py index 49eaa23e5..8e9c08ac2 100644 --- a/codeflash/optimization/optimizer.py +++ b/codeflash/optimization/optimizer.py @@ -41,6 +41,18 @@ from codeflash.models.models import BenchmarkKey, FunctionCalledInTest, ValidCode +def _extract_java_package_from_path(file_path: Path) -> str | None: + """Extract Java package from file path by finding src/main/java or src/test/java marker.""" + parts = file_path.parts + for i, part in enumerate(parts): + if part == "java" and i >= 2 and parts[i - 1] in ("main", "test") and parts[i - 2] == "src": + package_parts = parts[i + 1 : -1] # After java/, exclude filename + if package_parts: + return ".".join(package_parts) + return None + return None + + class Optimizer: def __init__(self, args: Namespace) -> None: self.args = args @@ -360,17 +372,40 @@ def rank_all_functions_globally( return all_functions try: - from codeflash.benchmarking.function_ranker import FunctionRanker + from codeflash.benchmarking.function_ranker import FunctionRanker, JavaFunctionRanker console.rule() logger.info("loading|Ranking functions globally by performance impact...") console.rule() - # Create ranker with trace data - ranker = FunctionRanker(trace_file_path) # Extract just the functions for ranking (without file paths) functions_only = [func for _, func in all_functions] + # Detect if functions are Java and use appropriate ranker + if functions_only and functions_only[0].language == "java": + from codeflash.languages.java.jfr_parser import JfrProfile + + # JFR file is alongside the trace DB with .jfr extension + jfr_file_path = trace_file_path.with_suffix(".jfr") + if not jfr_file_path.exists(): + logger.warning(f"JFR file not found: {jfr_file_path}, falling back to original order") + return all_functions + + # Extract packages from file paths (e.g., src/main/java/com/example/Workload.java → "com.example") + packages = set() + for func in functions_only: + package = _extract_java_package_from_path(func.file_path) + if package: + # Use top two levels as filter prefix (e.g., "com.example" from "com.example.sub") + parts = package.split(".") + packages.add(".".join(parts[: min(2, len(parts))])) + + jfr_profile = JfrProfile(jfr_file_path, list(packages)) + ranker = JavaFunctionRanker(jfr_profile) + else: + # Python ranker with trace data + ranker = FunctionRanker(trace_file_path) + # Rank globally ranked_functions = ranker.rank_functions(functions_only) diff --git a/codeflash/tracer.py b/codeflash/tracer.py index 199d07b6e..471795366 100644 --- a/codeflash/tracer.py +++ b/codeflash/tracer.py @@ -35,39 +35,79 @@ logger = logging.getLogger(__name__) -def main(args: Namespace | None = None) -> ArgumentParser: - # For non-Python languages, detect early and route to Optimizer - # Java, JavaScript, and TypeScript use their own test runners (Maven/JUnit, Jest) - # and should not go through Python tracing - if args is None and "--file" in sys.argv: +def _detect_non_python_language(args: Namespace | None) -> object | None: + """Detect if the project uses a non-Python language from --file or config. + + Returns a Language enum value if non-Python detected, None otherwise. + """ + from codeflash.languages.base import Language + + # Method 1: Check --file argument for non-Python file extension + file_path_to_check: Path | None = None + if args is not None and getattr(args, "file", None): + file_path_to_check = Path(args.file) + elif args is None and "--file" in sys.argv: try: file_idx = sys.argv.index("--file") if file_idx + 1 < len(sys.argv): - file_path = Path(sys.argv[file_idx + 1]) - if file_path.exists(): - from codeflash.languages import Language, get_language_support + file_path_to_check = Path(sys.argv[file_idx + 1]) + except (IndexError, ValueError): + pass - lang_support = get_language_support(file_path) - detected_language = lang_support.language + if file_path_to_check is not None and file_path_to_check.exists(): + try: + from codeflash.languages import get_language_support - if detected_language in (Language.JAVA, Language.JAVASCRIPT, Language.TYPESCRIPT): - # Parse and process args like main.py does - from codeflash.cli_cmds.cli import parse_args, process_pyproject_config + lang_support = get_language_support(file_path_to_check) + if lang_support.language != Language.PYTHON: + return lang_support.language + except Exception: + pass - full_args = parse_args() - full_args = process_pyproject_config(full_args) - # Set checkpoint functions to None (no checkpoint for single-file optimization) - full_args.previous_checkpoint_functions = None + # Method 2: Check project config for language field + try: + from codeflash.code_utils.config_parser import parse_config_file - from codeflash.optimization import optimizer + config_file = getattr(args, "config_file_path", None) if args else None + config, _ = parse_config_file(config_file) + lang_str = config.get("language", "") + if lang_str == "java": + return Language.JAVA + if lang_str in ("javascript", "typescript"): + return Language(lang_str) + except Exception: + pass - logger.info( - "Detected %s file, routing to Optimizer instead of Python tracer", detected_language.value - ) - optimizer.run_with_args(full_args) - return ArgumentParser() # Return dummy parser since we're done - except (IndexError, OSError, Exception): - pass # Fall through to normal tracing if detection fails + return None + + +def main(args: Namespace | None = None) -> ArgumentParser: + # For non-Python languages, detect early and route to the appropriate handler. + # Java, JavaScript, and TypeScript use their own test runners (Maven/JUnit, Jest) + # and should not go through Python tracing. + # + # Detection methods (in priority order): + # 1. --file pointing to a .java/.js/.ts file + # 2. language field in project config (codeflash.toml or pyproject.toml) + detected_language = _detect_non_python_language(args) + if detected_language is not None: + from codeflash.languages.base import Language + + if detected_language in (Language.JAVASCRIPT, Language.TYPESCRIPT): + from codeflash.cli_cmds.cli import parse_args, process_pyproject_config + from codeflash.optimization import optimizer + + full_args = parse_args() + full_args = process_pyproject_config(full_args) + full_args.previous_checkpoint_functions = None + logger.info( + "Detected %s project, routing to Optimizer instead of Python tracer", detected_language.value + ) + optimizer.run_with_args(full_args) + return ArgumentParser() + + if detected_language == Language.JAVA: + return _run_java_tracer(args) parser = ArgumentParser(allow_abbrev=False) parser.add_argument("-o", "--outfile", dest="outfile", help="Save trace to ", default="codeflash.trace") @@ -278,5 +318,92 @@ def main(args: Namespace | None = None) -> ArgumentParser: return parser +def _run_java_tracer(existing_args: Namespace | None = None) -> ArgumentParser: + """Run the Java two-stage tracer (JFR + argument capture) and optionally optimize.""" + from codeflash.cli_cmds.cli import parse_args, process_pyproject_config + + if existing_args is not None: + full_args = process_pyproject_config(existing_args) + else: + full_args = parse_args() + full_args = process_pyproject_config(full_args) + config = full_args + + trace_only = getattr(config, "trace_only", False) + project_root = Path(getattr(config, "project_root", ".")).resolve() + module_root = Path(getattr(config, "module_root", project_root)).resolve() + max_function_count = getattr(config, "max_function_count", 256) + timeout = int(getattr(config, "tracer_timeout", 0) or 0) + + from codeflash.code_utils.code_utils import get_run_tmp_file + from codeflash.languages.java.build_tools import find_test_root + from codeflash.languages.java.tracer import JavaTracer, run_java_tracer + + tracer = JavaTracer() + packages = tracer.detect_packages_from_source(module_root) + if not packages: + logger.warning("No Java packages detected in %s, will trace all non-JDK classes", module_root) + + trace_db_path = get_run_tmp_file(Path("java_trace.db")) + + # Place replay tests in the project's test source tree so Maven/Gradle can compile them + test_root = find_test_root(project_root) + if test_root: + output_dir = test_root / "codeflash" / "replay" + else: + output_dir = project_root / "src" / "test" / "java" / "codeflash" / "replay" + output_dir.mkdir(parents=True, exist_ok=True) + + # Remaining args after our flags are the Java command + remaining = sys.argv[sys.argv.index("--file") + 2 :] if "--file" in sys.argv else sys.argv[1:] + java_command = remaining if remaining else ["java", "-jar", "app.jar"] + + trace_db, jfr_file, test_count = run_java_tracer( + java_command=java_command, + trace_db_path=trace_db_path, + packages=packages, + project_root=project_root, + output_dir=output_dir, + max_function_count=max_function_count, + timeout=timeout, + ) + + console.print(f"[bold green]Java tracing complete:[/] {test_count} replay test files generated") + if jfr_file.exists(): + console.print(f" JFR profile: {jfr_file}") + if trace_db.exists(): + console.print(f" Trace DB: {trace_db}") + + if not trace_only and test_count > 0: + from codeflash.code_utils.config_consts import EffortLevel + from codeflash.languages import set_current_language + from codeflash.languages.base import Language + from codeflash.optimization import optimizer + + set_current_language(Language.JAVA) + + replay_test_paths = [p.resolve() for p in output_dir.glob("*.java")] + config.replay_test = replay_test_paths + config.previous_checkpoint_functions = None + config.effort = EffortLevel.HIGH.value + config.no_pr = True + config.file = None + config.function = None + config.test_project_root = project_root + optimizer.run_with_args(config) + + # Clean up generated replay tests + for replay_test_path in replay_test_paths: + Path(replay_test_path).unlink(missing_ok=True) + # Clean up codeflash/replay directory if empty + if output_dir.exists() and not any(output_dir.iterdir()): + output_dir.rmdir() + codeflash_dir = output_dir.parent + if codeflash_dir.exists() and codeflash_dir.name == "codeflash" and not any(codeflash_dir.iterdir()): + codeflash_dir.rmdir() + + return ArgumentParser() + + if __name__ == "__main__": main() diff --git a/codeflash/version.py b/codeflash/version.py index 2aabf8fc4..8212bcf10 100644 --- a/codeflash/version.py +++ b/codeflash/version.py @@ -1,2 +1,2 @@ # These version placeholders will be replaced by uv-dynamic-versioning during build. -__version__ = "0.20.3" +__version__ = "0.20.3.post136.dev0+030df218" diff --git a/tests/scripts/end_to_end_test_java_tracer.py b/tests/scripts/end_to_end_test_java_tracer.py new file mode 100644 index 000000000..6e065fb1d --- /dev/null +++ b/tests/scripts/end_to_end_test_java_tracer.py @@ -0,0 +1,127 @@ +import logging +import os +import pathlib +import re +import subprocess +import time + + +def run_test(expected_improvement_pct: int) -> bool: + logging.basicConfig(level=logging.INFO) + fixture_dir = (pathlib.Path(__file__).parent.parent / "test_languages" / "fixtures" / "java_tracer_e2e").resolve() + + # Compile the workload + classes_dir = fixture_dir / "target" / "classes" + classes_dir.mkdir(parents=True, exist_ok=True) + compile_result = subprocess.run( + [ + "javac", + "--release", + "11", + "-d", + str(classes_dir), + str(fixture_dir / "src" / "main" / "java" / "com" / "example" / "Workload.java"), + ], + capture_output=True, + text=True, + ) + if compile_result.returncode != 0: + logging.error(f"javac failed: {compile_result.stderr}") + return False + + # Run the Java tracer + optimizer + repo_root = pathlib.Path(__file__).parent.parent.parent + command = [ + "uv", + "run", + "--no-project", + str(repo_root / "codeflash" / "main.py"), + "optimize", + "--no-pr", + "java", + "-cp", + str(classes_dir), + "com.example.Workload", + ] + + env = os.environ.copy() + env["PYTHONIOENCODING"] = "utf-8" + process = subprocess.Popen( + command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + cwd=str(fixture_dir), + env=env, + encoding="utf-8", + ) + + output = [] + for line in process.stdout: + logging.info(line.strip()) + output.append(line) + + return_code = process.wait() + stdout = "".join(output) + + if return_code != 0: + logging.error(f"Command returned exit code {return_code}") + return False + + # Validate: replay tests were generated + if "replay test files generated" not in stdout: + logging.error("Failed to find replay test generation message") + return False + + # Validate: replay tests were discovered + replay_match = re.search(r"Discovered \d+ existing unit tests? and (\d+) replay tests?", stdout) + if not replay_match: + logging.error("Failed to find replay test discovery message") + return False + num_replay = int(replay_match.group(1)) + if num_replay == 0: + logging.error("No replay tests discovered") + return False + logging.info(f"Replay tests discovered: {num_replay}") + + # Validate: at least one optimization was found + if "⚡️ Optimization successful! 📄 " not in stdout: + logging.error("Failed to find optimization success message") + return False + + improvement_match = re.search(r"📈 ([\d,]+)% (?:(\w+) )?improvement", stdout) + if not improvement_match: + logging.error("Could not find improvement percentage in output") + return False + + improvement_pct = int(improvement_match.group(1).replace(",", "")) + logging.info(f"Performance improvement: {improvement_pct}%") + + if improvement_pct <= expected_improvement_pct: + logging.error(f"Performance improvement {improvement_pct}% not above {expected_improvement_pct}%") + return False + + logging.info(f"Success: Java tracer e2e passed with {improvement_pct}% improvement") + return True + + +def run_with_retries(test_func, *args) -> int: + max_retries = int(os.getenv("MAX_RETRIES", 3)) + retry_delay = int(os.getenv("RETRY_DELAY", 5)) + for attempt in range(1, max_retries + 1): + logging.info(f"\n=== Attempt {attempt} of {max_retries} ===") + if test_func(*args): + logging.info(f"Test passed on attempt {attempt}") + return 0 + logging.error(f"Test failed on attempt {attempt}") + if attempt < max_retries: + logging.info(f"Retrying in {retry_delay} seconds...") + time.sleep(retry_delay) + else: + logging.error("Test failed after all retries") + return 1 + return 1 + + +if __name__ == "__main__": + exit(run_with_retries(run_test, int(os.getenv("EXPECTED_IMPROVEMENT_PCT", 10)))) diff --git a/tests/test_languages/fixtures/java_tracer_e2e/codeflash.toml b/tests/test_languages/fixtures/java_tracer_e2e/codeflash.toml new file mode 100644 index 000000000..a501ef8cb --- /dev/null +++ b/tests/test_languages/fixtures/java_tracer_e2e/codeflash.toml @@ -0,0 +1,6 @@ +# Codeflash configuration for Java project + +[tool.codeflash] +module-root = "src/main/java" +tests-root = "src/test/java" +language = "java" diff --git a/tests/test_languages/fixtures/java_tracer_e2e/pom.xml b/tests/test_languages/fixtures/java_tracer_e2e/pom.xml new file mode 100644 index 000000000..7fffde8b2 --- /dev/null +++ b/tests/test_languages/fixtures/java_tracer_e2e/pom.xml @@ -0,0 +1,67 @@ + + 4.0.0 + + com.example + tracer-e2e + 1.0.0 + + + 11 + 11 + UTF-8 + + + + + org.junit.jupiter + junit-jupiter + 5.10.0 + test + + + com.codeflash + codeflash-runtime + 1.0.0 + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.2 + + + + org.jacoco + jacoco-maven-plugin + 0.8.13 + + + prepare-agent + + prepare-agent + + + + report + verify + + report + + + + + **/*.class + + + + + + + + diff --git a/tests/test_languages/fixtures/java_tracer_e2e/src/main/java/com/example/Workload.java b/tests/test_languages/fixtures/java_tracer_e2e/src/main/java/com/example/Workload.java new file mode 100644 index 000000000..b73f886ac --- /dev/null +++ b/tests/test_languages/fixtures/java_tracer_e2e/src/main/java/com/example/Workload.java @@ -0,0 +1,80 @@ +package com.example; + +import java.util.ArrayList; +import java.util.List; + +public class Workload { + + public static int computeSum(int n) { + if (n <= 0) { + return 0; + } + long nl = n; + long result = (nl * (nl - 1)) / 2; + return (int) result; + } + + public static String repeatString(String s, int count) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < count; i++) { + sb.append(s); + } + return sb.toString(); + } + + public static List filterEvens(List numbers) { + // Pre-size result to avoid repeated resizes. Keep same behavior for null input (will NPE). + List result = new ArrayList<>(numbers.size()); + + // Use indexed loop for RandomAccess lists (e.g., ArrayList) to avoid iterator allocation, + // but fall back to iterator-based (enhanced for) loop for non-random-access lists + // to prevent O(n^2) behavior on LinkedList. + if (numbers instanceof java.util.RandomAccess) { + for (int i = 0, sz = numbers.size(); i < sz; i++) { + Integer num = numbers.get(i); + if ((num & 1) == 0) { // faster parity check than modulus + result.add(num); + } + } + } else { + for (Integer n : numbers) { + if ((n & 1) == 0) { + result.add(n); + } + } + } + return result; + } + + public int instanceMethod(int x, int y) { + // Inline computeSum logic to avoid the static method call overhead on the hot path. + int sum; + if (x <= 0) { + sum = 0; + } else { + long nl = x; + long result = (nl * (nl - 1)) >> 1; + sum = (int) result; + } + return x * y + sum; + } + + public static void main(String[] args) { + // Exercise the methods so the tracer can capture invocations + System.out.println("computeSum(100) = " + computeSum(100)); + System.out.println("computeSum(50) = " + computeSum(50)); + + System.out.println("repeatString(\"ab\", 3) = " + repeatString("ab", 3)); + System.out.println("repeatString(\"x\", 5) = " + repeatString("x", 5)); + + List nums = new ArrayList<>(); + for (int i = 1; i <= 10; i++) nums.add(i); + System.out.println("filterEvens(1..10) = " + filterEvens(nums)); + + Workload w = new Workload(); + System.out.println("instanceMethod(5, 3) = " + w.instanceMethod(5, 3)); + System.out.println("instanceMethod(10, 2) = " + w.instanceMethod(10, 2)); + + System.out.println("Workload complete."); + } +} diff --git a/tests/test_languages/test_java/test_java_tracer_e2e.py b/tests/test_languages/test_java/test_java_tracer_e2e.py new file mode 100644 index 000000000..157f23eb6 --- /dev/null +++ b/tests/test_languages/test_java/test_java_tracer_e2e.py @@ -0,0 +1,304 @@ +from __future__ import annotations + +import sqlite3 +import subprocess +from pathlib import Path + +import pytest + +from codeflash.languages.java.line_profiler import find_agent_jar +from codeflash.languages.java.replay_test import generate_replay_tests, parse_replay_test_metadata +from codeflash.languages.java.tracer import ADD_OPENS_FLAGS, JavaTracer + +FIXTURE_DIR = Path(__file__).parent.parent / "fixtures" / "java_tracer_e2e" +WORKLOAD_SOURCE = FIXTURE_DIR / "src" / "main" / "java" / "com" / "example" / "Workload.java" +WORKLOAD_CLASS = "com.example.Workload" +WORKLOAD_PACKAGE = "com.example" + + +@pytest.fixture(scope="module") +def compiled_workload() -> Path: + """Compile the Java workload fixture (once per module).""" + classes_dir = FIXTURE_DIR / "target" / "classes" + classes_dir.mkdir(parents=True, exist_ok=True) + result = subprocess.run( + ["javac", "--release", "11", "-d", str(classes_dir), str(WORKLOAD_SOURCE)], + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, f"javac failed: {result.stderr}" + return classes_dir + + +@pytest.fixture +def trace_db(tmp_path: Path) -> Path: + return tmp_path / "trace.db" + + +class TestTracingAgent: + def test_agent_jar_found(self) -> None: + jar = find_agent_jar() + assert jar is not None, "codeflash-runtime JAR not found" + assert jar.exists() + + def test_agent_captures_invocations(self, compiled_workload: Path, trace_db: Path) -> None: + """Test that the tracing agent captures method invocations into SQLite.""" + agent_jar = find_agent_jar() + assert agent_jar is not None + + import json + + config = { + "dbPath": str(trace_db), + "packages": [WORKLOAD_PACKAGE], + "excludePackages": [], + "maxFunctionCount": 256, + "timeout": 0, + "projectRoot": str(FIXTURE_DIR), + } + config_path = trace_db.with_suffix(".config.json") + config_path.write_text(json.dumps(config), encoding="utf-8") + + result = subprocess.run( + [ + "java", + *ADD_OPENS_FLAGS.split(), + f"-javaagent:{agent_jar}=trace={config_path}", + "-cp", + str(compiled_workload), + WORKLOAD_CLASS, + ], + capture_output=True, + text=True, + check=False, + timeout=30, + ) + assert "Workload complete." in result.stdout, f"Workload failed to run: {result.stderr}" + assert trace_db.exists(), "Trace DB not created" + + # Verify database contents + conn = sqlite3.connect(str(trace_db)) + try: + rows = conn.execute("SELECT function, classname, descriptor, length(args) FROM function_calls").fetchall() + assert len(rows) >= 5, f"Expected at least 5 captured invocations, got {len(rows)}" + + # Check that specific methods were captured + functions = {row[0] for row in rows} + assert "computeSum" in functions + assert "repeatString" in functions + assert "filterEvens" in functions + assert "instanceMethod" in functions + + # Verify all rows have non-empty args blobs + for row in rows: + assert row[3] > 0, f"Empty args blob for {row[0]}" + + # Verify metadata + metadata = dict(conn.execute("SELECT key, value FROM metadata").fetchall()) + assert "totalCaptures" in metadata + assert int(metadata["totalCaptures"]) >= 5 + finally: + conn.close() + + def test_max_function_count_limit(self, compiled_workload: Path, trace_db: Path) -> None: + """Test that maxFunctionCount limits captures per method.""" + agent_jar = find_agent_jar() + assert agent_jar is not None + + import json + + config = { + "dbPath": str(trace_db), + "packages": [WORKLOAD_PACKAGE], + "excludePackages": [], + "maxFunctionCount": 2, + "timeout": 0, + "projectRoot": str(FIXTURE_DIR), + } + config_path = trace_db.with_suffix(".config.json") + config_path.write_text(json.dumps(config), encoding="utf-8") + + subprocess.run( + [ + "java", + *ADD_OPENS_FLAGS.split(), + f"-javaagent:{agent_jar}=trace={config_path}", + "-cp", + str(compiled_workload), + WORKLOAD_CLASS, + ], + capture_output=True, + text=True, + check=False, + timeout=30, + ) + + conn = sqlite3.connect(str(trace_db)) + try: + # computeSum is called 4 times (2 direct + 2 from instanceMethod) + compute_count = conn.execute( + "SELECT COUNT(*) FROM function_calls WHERE function = 'computeSum'" + ).fetchone()[0] + assert compute_count <= 2, f"Expected at most 2 computeSum captures, got {compute_count}" + finally: + conn.close() + + +class TestReplayTestGeneration: + def test_generates_test_files(self, compiled_workload: Path, trace_db: Path, tmp_path: Path) -> None: + """Test that replay test files are generated from trace DB.""" + # First, create a trace + agent_jar = find_agent_jar() + assert agent_jar is not None + + import json + + config = { + "dbPath": str(trace_db), + "packages": [WORKLOAD_PACKAGE], + "excludePackages": [], + "maxFunctionCount": 256, + "timeout": 0, + "projectRoot": str(FIXTURE_DIR), + } + config_path = trace_db.with_suffix(".config.json") + config_path.write_text(json.dumps(config), encoding="utf-8") + + subprocess.run( + [ + "java", + *ADD_OPENS_FLAGS.split(), + f"-javaagent:{agent_jar}=trace={config_path}", + "-cp", + str(compiled_workload), + WORKLOAD_CLASS, + ], + capture_output=True, + check=False, + timeout=30, + ) + + # Generate replay tests + output_dir = tmp_path / "replay_tests" + count = generate_replay_tests( + trace_db_path=trace_db, + output_dir=output_dir, + project_root=FIXTURE_DIR, + ) + + assert count >= 1, f"Expected at least 1 test file, got {count}" + test_files = list(output_dir.glob("*.java")) + assert len(test_files) >= 1 + + # Find the main workload test file + workload_files = [f for f in test_files if "Workload" in f.name and "ConstructorAccess" not in f.name] + assert len(workload_files) == 1 + content = workload_files[0].read_text(encoding="utf-8") + assert "package codeflash.replay;" in content + assert "import org.junit.jupiter.api.Test;" in content + assert "ReplayHelper" in content + assert "replay_computeSum_0" in content + assert "replay_repeatString_0" in content + + def test_metadata_parsing(self, compiled_workload: Path, trace_db: Path, tmp_path: Path) -> None: + """Test that metadata comments are correctly parsed from generated tests.""" + agent_jar = find_agent_jar() + assert agent_jar is not None + + import json + + config = { + "dbPath": str(trace_db), + "packages": [WORKLOAD_PACKAGE], + "excludePackages": [], + "maxFunctionCount": 256, + "timeout": 0, + "projectRoot": str(FIXTURE_DIR), + } + config_path = trace_db.with_suffix(".config.json") + config_path.write_text(json.dumps(config), encoding="utf-8") + + subprocess.run( + [ + "java", + *ADD_OPENS_FLAGS.split(), + f"-javaagent:{agent_jar}=trace={config_path}", + "-cp", + str(compiled_workload), + WORKLOAD_CLASS, + ], + capture_output=True, + check=False, + timeout=30, + ) + + output_dir = tmp_path / "replay_tests" + generate_replay_tests(trace_db_path=trace_db, output_dir=output_dir, project_root=FIXTURE_DIR) + + test_files = [f for f in output_dir.glob("*.java") if "ConstructorAccess" not in f.name] + test_file = test_files[0] + metadata = parse_replay_test_metadata(test_file) + + assert "functions" in metadata + assert "trace_file" in metadata + assert "classname" in metadata + assert "computeSum" in metadata["functions"] + assert metadata["classname"] == "com.example.Workload" + assert metadata["trace_file"] == trace_db.as_posix() + + +class TestJavaTracerOrchestration: + def test_two_stage_trace(self, compiled_workload: Path, tmp_path: Path) -> None: + """Test the full two-stage JavaTracer flow (JFR + agent).""" + trace_db_path = tmp_path / "trace.db" + tracer = JavaTracer() + + trace_db, _jfr_file = tracer.trace( + java_command=["java", "-cp", str(compiled_workload), WORKLOAD_CLASS], + trace_db_path=trace_db_path, + packages=[WORKLOAD_PACKAGE], + project_root=FIXTURE_DIR, + ) + + assert trace_db.exists(), "Trace DB not created by JavaTracer" + + # Verify trace DB has captures + conn = sqlite3.connect(str(trace_db)) + try: + count = conn.execute("SELECT COUNT(*) FROM function_calls").fetchone()[0] + assert count >= 5, f"Expected at least 5 captured invocations, got {count}" + finally: + conn.close() + + def test_full_trace_and_replay_generation(self, compiled_workload: Path, tmp_path: Path) -> None: + """Test the full flow: trace → generate replay tests.""" + from codeflash.languages.java.tracer import run_java_tracer + + trace_db_path = tmp_path / "trace.db" + output_dir = tmp_path / "replay_tests" + + trace_db, _jfr_file, test_count = run_java_tracer( + java_command=["java", "-cp", str(compiled_workload), WORKLOAD_CLASS], + trace_db_path=trace_db_path, + packages=[WORKLOAD_PACKAGE], + project_root=FIXTURE_DIR, + output_dir=output_dir, + ) + + assert trace_db.exists() + assert test_count >= 1 + + # Verify the generated test files + test_files = list(output_dir.glob("*.java")) + assert len(test_files) >= 1 + workload_files = [f for f in test_files if "Workload" in f.name and "ConstructorAccess" not in f.name] + assert len(workload_files) == 1 + content = workload_files[0].read_text(encoding="utf-8") + assert "replay_computeSum" in content + assert "replay_instanceMethod" in content + + def test_package_detection(self) -> None: + """Test that package detection finds Java packages from source files.""" + packages = JavaTracer.detect_packages_from_source(FIXTURE_DIR) + assert "com.example" in packages diff --git a/tests/test_languages/test_java/test_java_tracer_integration.py b/tests/test_languages/test_java/test_java_tracer_integration.py new file mode 100644 index 000000000..f6ffefdf2 --- /dev/null +++ b/tests/test_languages/test_java/test_java_tracer_integration.py @@ -0,0 +1,345 @@ +"""End-to-end integration test for the Java tracer → optimizer pipeline. + +Tests the full flow: trace → replay test generation → function discovery → +test discovery → function ranking, using the simple Workload fixture. +""" + +from __future__ import annotations + +import subprocess +from pathlib import Path + +import pytest + +from codeflash.languages.java.tracer import run_java_tracer + +FIXTURE_DIR = Path(__file__).parent.parent / "fixtures" / "java_tracer_e2e" +WORKLOAD_SOURCE = FIXTURE_DIR / "src" / "main" / "java" / "com" / "example" / "Workload.java" +WORKLOAD_CLASS = "com.example.Workload" +WORKLOAD_PACKAGE = "com.example" + + +@pytest.fixture(scope="module") +def compiled_workload() -> Path: + classes_dir = FIXTURE_DIR / "target" / "classes" + classes_dir.mkdir(parents=True, exist_ok=True) + result = subprocess.run( + ["javac", "--release", "11", "-d", str(classes_dir), str(WORKLOAD_SOURCE)], + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, f"javac failed: {result.stderr}" + return classes_dir + + +@pytest.fixture +def traced_workload(compiled_workload: Path, tmp_path: Path) -> tuple[Path, Path, Path, int]: + """Trace the workload and generate replay tests. Returns (trace_db, jfr_file, output_dir, test_count).""" + trace_db_path = tmp_path / "trace.db" + output_dir = tmp_path / "replay_tests" + + trace_db, jfr_file, test_count = run_java_tracer( + java_command=["java", "-cp", str(compiled_workload), WORKLOAD_CLASS], + trace_db_path=trace_db_path, + packages=[WORKLOAD_PACKAGE], + project_root=FIXTURE_DIR, + output_dir=output_dir, + ) + + assert trace_db.exists(), "Trace DB not created" + assert test_count >= 1, f"Expected at least 1 replay test file, got {test_count}" + return trace_db, jfr_file, output_dir, test_count + + +class TestFunctionDiscoveryFromReplayTests: + """Test that functions are correctly discovered from replay test metadata.""" + + def test_discover_functions_from_replay_tests(self, traced_workload: tuple) -> None: + _trace_db, _jfr_file, output_dir, _test_count = traced_workload + + from codeflash.discovery.functions_to_optimize import _get_java_replay_test_functions + from codeflash.verification.verification_utils import TestConfig + + replay_test_paths = list(output_dir.glob("*.java")) + assert len(replay_test_paths) >= 1 + + test_cfg = TestConfig( + tests_root=FIXTURE_DIR / "src" / "test" / "java", + tests_project_rootdir=FIXTURE_DIR, + project_root_path=FIXTURE_DIR, + pytest_cmd="pytest", + ) + + functions, trace_file_path = _get_java_replay_test_functions(replay_test_paths, test_cfg, FIXTURE_DIR) + + # Should have found functions in the Workload source file + assert len(functions) > 0, "No functions discovered from replay tests" + assert trace_file_path.exists(), f"Trace file not found: {trace_file_path}" + + # Collect all discovered function names + all_func_names = set() + for file_path, func_list in functions.items(): + assert file_path.exists(), f"Source file not found: {file_path}" + assert "Workload" in file_path.name + for func in func_list: + all_func_names.add(func.function_name) + assert func.language == "java", f"Expected language='java', got '{func.language}'" + assert func.file_path == file_path + + assert "computeSum" in all_func_names + assert "repeatString" in all_func_names + + def test_discover_tests_for_replay_tests(self, traced_workload: tuple) -> None: + """Test that test discovery maps replay tests to source functions.""" + _trace_db, _jfr_file, output_dir, _test_count = traced_workload + + from codeflash.languages.java.discovery import discover_functions_from_source + from codeflash.languages.java.test_discovery import discover_tests + + source_code = WORKLOAD_SOURCE.read_text(encoding="utf-8") + source_functions = discover_functions_from_source(source_code, file_path=WORKLOAD_SOURCE) + + result = discover_tests(output_dir, source_functions) + + # Replay tests should be mapped to source functions + assert len(result) > 0, "No test mappings found from replay tests" + + # Check specific functions are mapped + matched_func_names = set() + for qualified_name in result: + func_name = qualified_name.split(".")[-1] if "." in qualified_name else qualified_name + matched_func_names.add(func_name) + + assert "computeSum" in matched_func_names, f"computeSum not found in: {result.keys()}" + assert "repeatString" in matched_func_names, f"repeatString not found in: {result.keys()}" + + # Each function should have at least one test + for func_name, test_infos in result.items(): + assert len(test_infos) > 0, f"No tests for {func_name}" + for test_info in test_infos: + assert test_info.test_file.exists() + assert "ReplayTest" in test_info.test_file.name + + +class TestJfrProfiling: + """Test JFR profiling and function ranking.""" + + def test_jfr_parsing(self, traced_workload: tuple) -> None: + _trace_db, jfr_file, _output_dir, _test_count = traced_workload + + if not jfr_file.exists(): + pytest.skip("JFR file not created (JFR may not be available)") + + from codeflash.languages.java.jfr_parser import JfrProfile + + profile = JfrProfile(jfr_file, [WORKLOAD_PACKAGE]) + ranking = profile.get_method_ranking() + + # The workload is very short, so JFR might not capture many samples + # Just verify the parser doesn't crash and returns a list + assert isinstance(ranking, list) + + def test_java_function_ranker(self, traced_workload: tuple) -> None: + _trace_db, jfr_file, _output_dir, _test_count = traced_workload + + if not jfr_file.exists(): + pytest.skip("JFR file not created (JFR may not be available)") + + from codeflash.benchmarking.function_ranker import JavaFunctionRanker + from codeflash.languages.java.discovery import discover_functions_from_source + from codeflash.languages.java.jfr_parser import JfrProfile + + profile = JfrProfile(jfr_file, [WORKLOAD_PACKAGE]) + ranker = JavaFunctionRanker(profile) + + source_code = WORKLOAD_SOURCE.read_text(encoding="utf-8") + source_functions = discover_functions_from_source(source_code, file_path=WORKLOAD_SOURCE) + + # Rank functions - should not crash even with minimal JFR data + ranked = ranker.rank_functions(source_functions) + assert isinstance(ranked, list) + + +class TestFullDiscoveryPipeline: + """Test the complete discovery pipeline as the optimizer would run it.""" + + def test_full_pipeline(self, compiled_workload: Path, tmp_path: Path) -> None: + """Simulate what optimizer.run() does: discover functions, discover tests, rank. + + Uses the same directory layout as the real flow: replay tests go into + src/test/java/codeflash/replay/ so test discovery can find them. + """ + trace_db_path = tmp_path / "trace.db" + + # Generate replay tests into the project's test source tree (like _run_java_tracer does) + test_root = FIXTURE_DIR / "src" / "test" / "java" + output_dir = test_root / "codeflash" / "replay" + output_dir.mkdir(parents=True, exist_ok=True) + + try: + _trace_db, jfr_file, test_count = run_java_tracer( + java_command=["java", "-cp", str(compiled_workload), WORKLOAD_CLASS], + trace_db_path=trace_db_path, + packages=[WORKLOAD_PACKAGE], + project_root=FIXTURE_DIR, + output_dir=output_dir, + ) + assert test_count >= 1 + + # Step 1: Discover functions from replay tests (like get_optimizable_functions) + from codeflash.discovery.functions_to_optimize import _get_java_replay_test_functions + from codeflash.verification.verification_utils import TestConfig + + replay_test_paths = list(output_dir.glob("*.java")) + test_cfg = TestConfig( + tests_root=test_root, + tests_project_rootdir=FIXTURE_DIR, + project_root_path=FIXTURE_DIR, + pytest_cmd="pytest", + ) + + file_to_funcs, trace_file_path = _get_java_replay_test_functions(replay_test_paths, test_cfg, FIXTURE_DIR) + assert len(file_to_funcs) > 0 + assert trace_file_path.exists() + + # Step 2: Set language (like optimizer.run lines 496-502) + from codeflash.languages import set_current_language + from codeflash.languages.base import Language + + set_current_language(Language.JAVA) + + # Step 3: Discover tests (like optimizer.discover_tests) + from codeflash.discovery.discover_unit_tests import discover_tests_for_language + + all_functions = [func for funcs in file_to_funcs.values() for func in funcs] + function_to_tests, num_unit_tests, num_replay_tests = discover_tests_for_language( + test_cfg, "java", file_to_funcs + ) + + assert num_unit_tests + num_replay_tests > 0, "No tests discovered" + assert num_replay_tests > 0, f"Expected replay tests, got {num_replay_tests}" + assert len(function_to_tests) > 0, "No function-to-test mappings" + + # Verify function_to_tests has entries for our traced functions + has_compute_sum = any("computeSum" in key for key in function_to_tests) + assert has_compute_sum, f"computeSum not in function_to_tests keys: {list(function_to_tests.keys())}" + + # Step 4: Rank functions (like optimizer.rank_all_functions_globally) + if jfr_file.exists(): + from codeflash.benchmarking.function_ranker import JavaFunctionRanker + from codeflash.languages.java.jfr_parser import JfrProfile + + packages = set() + for func in all_functions: + parts = func.qualified_name.split(".") + if len(parts) >= 2: + packages.add(".".join(parts[:-1])) + + profile = JfrProfile(jfr_file, list(packages)) + ranker = JavaFunctionRanker(profile) + ranked = ranker.rank_functions(all_functions) + assert isinstance(ranked, list) + + finally: + # Clean up generated replay tests from fixture directory + for f in output_dir.glob("*.java"): + f.unlink() + if output_dir.exists() and not any(output_dir.iterdir()): + output_dir.rmdir() + codeflash_dir = output_dir.parent + if codeflash_dir.exists() and codeflash_dir.name == "codeflash" and not any(codeflash_dir.iterdir()): + codeflash_dir.rmdir() + + def test_instrument_and_compile_replay_tests(self, compiled_workload: Path, tmp_path: Path) -> None: + """Test that replay tests can be instrumented and compiled by Maven.""" + trace_db_path = tmp_path / "trace.db" + + test_root = FIXTURE_DIR / "src" / "test" / "java" + output_dir = test_root / "codeflash" / "replay" + output_dir.mkdir(parents=True, exist_ok=True) + + cleanup_paths: list[Path] = [] + try: + _trace_db, _jfr_file, test_count = run_java_tracer( + java_command=["java", "-cp", str(compiled_workload), WORKLOAD_CLASS], + trace_db_path=trace_db_path, + packages=[WORKLOAD_PACKAGE], + project_root=FIXTURE_DIR, + output_dir=output_dir, + ) + assert test_count >= 1 + + replay_test_paths = list(output_dir.glob("*.java")) + cleanup_paths.extend(replay_test_paths) + + # Instrument a replay test (like instrument_existing_tests does) + from codeflash.languages.java.discovery import discover_functions_from_source + from codeflash.languages.java.instrumentation import instrument_existing_test + + source_code = WORKLOAD_SOURCE.read_text(encoding="utf-8") + source_functions = discover_functions_from_source(source_code, file_path=WORKLOAD_SOURCE) + # Pick the first function with a return type for instrumentation + target_func = next(f for f in source_functions if f.function_name == "computeSum") + + replay_test_file = replay_test_paths[0] + test_source = replay_test_file.read_text(encoding="utf-8") + + # Instrument for behavior mode + success, instrumented_source = instrument_existing_test( + test_string=test_source, function_to_optimize=target_func, mode="behavior", test_path=replay_test_file + ) + assert success, "Failed to instrument replay test for behavior mode" + assert instrumented_source is not None + assert "__perfinstrumented" in instrumented_source + + # Write the instrumented test + instrumented_path = replay_test_file.parent / f"{replay_test_file.stem}__perfinstrumented.java" + instrumented_path.write_text(instrumented_source, encoding="utf-8") + cleanup_paths.append(instrumented_path) + + # Instrument for performance mode + success, perf_source = instrument_existing_test( + test_string=test_source, + function_to_optimize=target_func, + mode="performance", + test_path=replay_test_file, + ) + assert success, "Failed to instrument replay test for performance mode" + assert perf_source is not None + + perf_path = replay_test_file.parent / f"{replay_test_file.stem}__perfonlyinstrumented.java" + perf_path.write_text(perf_source, encoding="utf-8") + cleanup_paths.append(perf_path) + + # Install codeflash-runtime as Maven dependency and compile + from codeflash.languages.java.build_tool_strategy import get_strategy + + strategy = get_strategy(FIXTURE_DIR) + strategy.ensure_runtime(FIXTURE_DIR, None) + + import os + + compile_env = os.environ.copy() + compile_result = strategy.compile_tests(FIXTURE_DIR, compile_env, None, timeout=120) + + assert compile_result.returncode == 0, ( + f"Maven compilation failed (rc={compile_result.returncode}):\n" + f"stdout: {compile_result.stdout}\n" + f"stderr: {compile_result.stderr}" + ) + + finally: + for f in cleanup_paths: + f.unlink(missing_ok=True) + # Also clean up Maven build artifacts for the replay package + replay_classes = FIXTURE_DIR / "target" / "test-classes" / "codeflash" + if replay_classes.exists(): + import shutil + + shutil.rmtree(replay_classes, ignore_errors=True) + if output_dir.exists() and not any(output_dir.iterdir()): + output_dir.rmdir() + codeflash_dir = output_dir.parent + if codeflash_dir.exists() and codeflash_dir.name == "codeflash" and not any(codeflash_dir.iterdir()): + codeflash_dir.rmdir() diff --git a/tests/test_languages/test_java/test_test_discovery.py b/tests/test_languages/test_java/test_test_discovery.py index 1644a272a..d6830389c 100644 --- a/tests/test_languages/test_java/test_test_discovery.py +++ b/tests/test_languages/test_java/test_test_discovery.py @@ -557,3 +557,87 @@ def test_tests_suffix_pattern(self, tmp_path: Path): # CalculatorTests should match Calculator class assert len(result) > 0 assert "Calculator.add" in result + + +class TestReplayTestDiscovery: + """Tests for replay test file discovery.""" + + def test_discover_replay_tests(self, tmp_path: Path): + """Test that replay test files are discovered and mapped to source functions.""" + src_file = tmp_path / "Calculator.java" + src_file.write_text( + """ +public class Calculator { + public int add(int a, int b) { return a + b; } + public int multiply(int a, int b) { return a * b; } +} +""" + ) + + test_dir = tmp_path / "test" + test_dir.mkdir() + replay_test = test_dir / "ReplayTest_Calculator.java" + replay_test.write_text( + """// codeflash:functions=add,multiply +// codeflash:trace_file=/tmp/trace.db +// codeflash:classname=Calculator +package codeflash.replay; + +import org.junit.jupiter.api.Test; +import com.codeflash.ReplayHelper; + +class ReplayTest_Calculator { + private static final ReplayHelper helper = + new ReplayHelper("/tmp/trace.db"); + + @Test void replay_add_0() throws Exception { + helper.replay("Calculator", "add", "(II)I", 0); + } + + @Test void replay_multiply_0() throws Exception { + helper.replay("Calculator", "multiply", "(II)I", 0); + } +} +""" + ) + + source_functions = discover_functions_from_source(src_file.read_text(), file_path=src_file) + result = discover_tests(test_dir, source_functions) + + assert "Calculator.add" in result + assert "Calculator.multiply" in result + assert len(result["Calculator.add"]) == 2 # Both replay_add_0 and replay_multiply_0 mapped + assert len(result["Calculator.multiply"]) == 2 + + def test_replay_tests_not_confused_with_regular_tests(self, tmp_path: Path): + """Test that files without codeflash metadata are not treated as replay tests.""" + src_file = tmp_path / "Calculator.java" + src_file.write_text( + """ +public class Calculator { + public int add(int a, int b) { return a + b; } +} +""" + ) + + test_dir = tmp_path / "test" + test_dir.mkdir() + regular_test = test_dir / "CalculatorTest.java" + regular_test.write_text( + """ +import org.junit.jupiter.api.Test; +public class CalculatorTest { + @Test + public void testAdd() { + Calculator calc = new Calculator(); + calc.add(1, 2); + } +} +""" + ) + + source_functions = discover_functions_from_source(src_file.read_text(), file_path=src_file) + result = discover_tests(test_dir, source_functions) + + # Should find through regular static analysis, not replay metadata + assert "Calculator.add" in result From 0a83002555dc10048e723db83186dbbcbd43864d Mon Sep 17 00:00:00 2001 From: misrasaurabh1 Date: Wed, 18 Mar 2026 23:17:27 -0700 Subject: [PATCH 02/12] fix: Java tracer e2e test script for CI compatibility - Use `uv run -m codeflash.main` instead of direct file path - Remove redundant --no-pr (already hardcoded in _run_java_tracer) - Clean up leftover replay tests between retry attempts - Add error logging for subprocess output Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/scripts/end_to_end_test_java_tracer.py | 22 +++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/tests/scripts/end_to_end_test_java_tracer.py b/tests/scripts/end_to_end_test_java_tracer.py index 6e065fb1d..6485c07dc 100644 --- a/tests/scripts/end_to_end_test_java_tracer.py +++ b/tests/scripts/end_to_end_test_java_tracer.py @@ -2,6 +2,7 @@ import os import pathlib import re +import shutil import subprocess import time @@ -10,6 +11,18 @@ def run_test(expected_improvement_pct: int) -> bool: logging.basicConfig(level=logging.INFO) fixture_dir = (pathlib.Path(__file__).parent.parent / "test_languages" / "fixtures" / "java_tracer_e2e").resolve() + # Clean up leftover replay tests from previous runs + replay_dir = fixture_dir / "src" / "test" / "java" / "codeflash" / "replay" + if replay_dir.exists(): + shutil.rmtree(replay_dir, ignore_errors=True) + # Also clean up any instrumented test files + test_java_dir = fixture_dir / "src" / "test" / "java" + if test_java_dir.exists(): + for f in test_java_dir.rglob("*__perfinstrumented*.java"): + f.unlink(missing_ok=True) + for f in test_java_dir.rglob("*__perfonlyinstrumented*.java"): + f.unlink(missing_ok=True) + # Compile the workload classes_dir = fixture_dir / "target" / "classes" classes_dir.mkdir(parents=True, exist_ok=True) @@ -30,14 +43,13 @@ def run_test(expected_improvement_pct: int) -> bool: return False # Run the Java tracer + optimizer - repo_root = pathlib.Path(__file__).parent.parent.parent command = [ "uv", "run", "--no-project", - str(repo_root / "codeflash" / "main.py"), + "-m", + "codeflash.main", "optimize", - "--no-pr", "java", "-cp", str(classes_dir), @@ -46,6 +58,8 @@ def run_test(expected_improvement_pct: int) -> bool: env = os.environ.copy() env["PYTHONIOENCODING"] = "utf-8" + logging.info(f"Running command: {' '.join(command)}") + logging.info(f"Working directory: {fixture_dir}") process = subprocess.Popen( command, stdout=subprocess.PIPE, @@ -63,6 +77,8 @@ def run_test(expected_improvement_pct: int) -> bool: return_code = process.wait() stdout = "".join(output) + if return_code != 0: + logging.error(f"Full output:\n{stdout}") if return_code != 0: logging.error(f"Command returned exit code {return_code}") From c31f83726ac0895355593a4861fb87ef2dfdb1cd Mon Sep 17 00:00:00 2001 From: misrasaurabh1 Date: Wed, 18 Mar 2026 23:19:44 -0700 Subject: [PATCH 03/12] fix: ensure src/test/java directory exists before config validation Git doesn't track empty directories, so src/test/java must be created before process_pyproject_config validates tests-root exists. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/scripts/end_to_end_test_java_tracer.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/scripts/end_to_end_test_java_tracer.py b/tests/scripts/end_to_end_test_java_tracer.py index 6485c07dc..e904a4e98 100644 --- a/tests/scripts/end_to_end_test_java_tracer.py +++ b/tests/scripts/end_to_end_test_java_tracer.py @@ -11,17 +11,18 @@ def run_test(expected_improvement_pct: int) -> bool: logging.basicConfig(level=logging.INFO) fixture_dir = (pathlib.Path(__file__).parent.parent / "test_languages" / "fixtures" / "java_tracer_e2e").resolve() + # Ensure test directory exists (git doesn't track empty dirs) + test_java_dir = fixture_dir / "src" / "test" / "java" + test_java_dir.mkdir(parents=True, exist_ok=True) + # Clean up leftover replay tests from previous runs - replay_dir = fixture_dir / "src" / "test" / "java" / "codeflash" / "replay" + replay_dir = test_java_dir / "codeflash" / "replay" if replay_dir.exists(): shutil.rmtree(replay_dir, ignore_errors=True) - # Also clean up any instrumented test files - test_java_dir = fixture_dir / "src" / "test" / "java" - if test_java_dir.exists(): - for f in test_java_dir.rglob("*__perfinstrumented*.java"): - f.unlink(missing_ok=True) - for f in test_java_dir.rglob("*__perfonlyinstrumented*.java"): - f.unlink(missing_ok=True) + for f in test_java_dir.rglob("*__perfinstrumented*.java"): + f.unlink(missing_ok=True) + for f in test_java_dir.rglob("*__perfonlyinstrumented*.java"): + f.unlink(missing_ok=True) # Compile the workload classes_dir = fixture_dir / "target" / "classes" From d5ca52af8840c42e0293e7db7cc68473def2b624 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 06:29:35 +0000 Subject: [PATCH 04/12] style: fix ruff formatting and revert auto-generated version - Unwrap logger.info call in tracer.py that fits within 120-char limit - Revert auto-generated dev version string in version.py back to 0.20.3 Co-authored-by: Claude Sonnet 4.6 --- codeflash/tracer.py | 4 +--- codeflash/version.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/codeflash/tracer.py b/codeflash/tracer.py index 471795366..6f03d9df3 100644 --- a/codeflash/tracer.py +++ b/codeflash/tracer.py @@ -100,9 +100,7 @@ def main(args: Namespace | None = None) -> ArgumentParser: full_args = parse_args() full_args = process_pyproject_config(full_args) full_args.previous_checkpoint_functions = None - logger.info( - "Detected %s project, routing to Optimizer instead of Python tracer", detected_language.value - ) + logger.info("Detected %s project, routing to Optimizer instead of Python tracer", detected_language.value) optimizer.run_with_args(full_args) return ArgumentParser() diff --git a/codeflash/version.py b/codeflash/version.py index 8212bcf10..2aabf8fc4 100644 --- a/codeflash/version.py +++ b/codeflash/version.py @@ -1,2 +1,2 @@ # These version placeholders will be replaced by uv-dynamic-versioning during build. -__version__ = "0.20.3.post136.dev0+030df218" +__version__ = "0.20.3" From d589aa0a1bec97ecc71b266c45c448538c89e8aa Mon Sep 17 00:00:00 2001 From: "codeflash-ai[bot]" <148906541+codeflash-ai[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 06:41:58 +0000 Subject: [PATCH 05/12] Optimize JavaFunctionRanker.get_function_addressable_time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original code performed a linear scan over `self._ranking` on every call to `get_function_addressable_time`, which `rank_functions` invokes repeatedly (once per function to filter, plus once per function to sort). The optimized version builds a hash map `_ranking_by_name` during `__init__`, replacing the O(n) loop with an O(1) dictionary lookup. Line profiler confirms the loop and comparison accounted for 94.7% of original runtime. When `rank_functions` calls `get_function_addressable_time` dozens or hundreds of times across a 1000-method ranking (as in `test_large_number_of_methods_and_repeated_queries_perf_and_correctness`), the lookup cost drops from ~293 µs to ~10 µs per call, yielding the 1244% overall speedup. The optimization also consolidates the two calls to `get_addressable_time_ns` in `get_function_stats_summary` into a single call, stored in a local variable, eliminating redundant work. --- codeflash/benchmarking/function_ranker.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/codeflash/benchmarking/function_ranker.py b/codeflash/benchmarking/function_ranker.py index 7337c3c4d..a77513a30 100644 --- a/codeflash/benchmarking/function_ranker.py +++ b/codeflash/benchmarking/function_ranker.py @@ -62,6 +62,11 @@ class JavaFunctionRanker: def __init__(self, jfr_profile: JfrProfile) -> None: self._jfr_profile = jfr_profile self._ranking = jfr_profile.get_method_ranking() + self._ranking_by_name: dict[str, dict] = {} + for entry in self._ranking: + name = entry["method_name"] + if name not in self._ranking_by_name: + self._ranking_by_name[name] = entry def get_function_stats_summary(self, function_to_optimize: FunctionToOptimize) -> dict | None: for entry in self._ranking: @@ -81,8 +86,10 @@ def get_function_stats_summary(self, function_to_optimize: FunctionToOptimize) - return None def get_function_addressable_time(self, function_to_optimize: FunctionToOptimize) -> float: - stats = self.get_function_stats_summary(function_to_optimize) - return stats["addressable_time_ns"] if stats else 0.0 + entry = self._ranking_by_name.get(function_to_optimize.function_name) + if entry is None: + return 0.0 + return self._jfr_profile.get_addressable_time_ns(entry["class_name"], entry["method_name"]) def rank_functions( self, functions_to_optimize: list[FunctionToOptimize], min_functions: int = 5 From 5d8ca3bd8532fca0e13a994fac9435fb2f24d4a5 Mon Sep 17 00:00:00 2001 From: misrasaurabh1 Date: Thu, 19 Mar 2026 00:04:45 -0700 Subject: [PATCH 06/12] fix: timeout handling and block comment parsing for Java tracer - Read --timeout from both config.timeout and config.tracer_timeout - Handle multi-line /* */ block comments in package detection (aerospike source files start with license block comments before package declaration) Co-Authored-By: Claude Opus 4.6 (1M context) --- codeflash/languages/java/tracer.py | 12 ++++++++++-- codeflash/tracer.py | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/codeflash/languages/java/tracer.py b/codeflash/languages/java/tracer.py index 94127b884..7b5a30421 100644 --- a/codeflash/languages/java/tracer.py +++ b/codeflash/languages/java/tracer.py @@ -118,17 +118,25 @@ def detect_packages_from_source(module_root: Path) -> list[str]: packages: set[str] = set() for java_file in module_root.rglob("*.java"): try: + in_block_comment = False with java_file.open("r", encoding="utf-8") as f: for line in f: stripped = line.strip() + if in_block_comment: + if "*/" in stripped: + in_block_comment = False + continue + if stripped.startswith("/*"): + if "*/" not in stripped: + in_block_comment = True + continue if stripped.startswith("package "): pkg = stripped[8:].rstrip(";").strip() - # Use top two levels as prefix (e.g., "com.aerospike") parts = pkg.split(".") prefix = ".".join(parts[: min(2, len(parts))]) packages.add(prefix) break - if stripped and not stripped.startswith("//") and not stripped.startswith("/*"): + if stripped and not stripped.startswith("//"): break except (OSError, UnicodeDecodeError): continue diff --git a/codeflash/tracer.py b/codeflash/tracer.py index 6f03d9df3..dbc5a5a04 100644 --- a/codeflash/tracer.py +++ b/codeflash/tracer.py @@ -331,7 +331,7 @@ def _run_java_tracer(existing_args: Namespace | None = None) -> ArgumentParser: project_root = Path(getattr(config, "project_root", ".")).resolve() module_root = Path(getattr(config, "module_root", project_root)).resolve() max_function_count = getattr(config, "max_function_count", 256) - timeout = int(getattr(config, "tracer_timeout", 0) or 0) + timeout = int(getattr(config, "timeout", None) or getattr(config, "tracer_timeout", 0) or 0) from codeflash.code_utils.code_utils import get_run_tmp_file from codeflash.languages.java.build_tools import find_test_root From 55c6d299187539a58a37364b0e140b9b3b39e163 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 07:13:16 +0000 Subject: [PATCH 07/12] fix: resolve mypy type errors in Java tracer, jfr_parser, and function_ranker - Import Language from codeflash.languages (exported) not codeflash.languages.base - Fix _detect_non_python_language return type: object | None -> Language | None - Fix bare dict type annotations: dict -> dict[str, Any] in jfr_parser.py and function_ranker.py - Fix pytest_splits/test_paths type narrowing by separating assignment from None check Co-authored-by: Saurabh Misra --- codeflash/benchmarking/function_ranker.py | 6 +++--- codeflash/languages/java/jfr_parser.py | 7 ++++--- codeflash/tracer.py | 24 +++++++++++++---------- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/codeflash/benchmarking/function_ranker.py b/codeflash/benchmarking/function_ranker.py index a77513a30..da565c6d7 100644 --- a/codeflash/benchmarking/function_ranker.py +++ b/codeflash/benchmarking/function_ranker.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from codeflash.cli_cmds.console import logger from codeflash.code_utils.config_consts import DEFAULT_IMPORTANCE_THRESHOLD @@ -62,13 +62,13 @@ class JavaFunctionRanker: def __init__(self, jfr_profile: JfrProfile) -> None: self._jfr_profile = jfr_profile self._ranking = jfr_profile.get_method_ranking() - self._ranking_by_name: dict[str, dict] = {} + self._ranking_by_name: dict[str, dict[str, Any]] = {} for entry in self._ranking: name = entry["method_name"] if name not in self._ranking_by_name: self._ranking_by_name[name] = entry - def get_function_stats_summary(self, function_to_optimize: FunctionToOptimize) -> dict | None: + def get_function_stats_summary(self, function_to_optimize: FunctionToOptimize) -> dict[str, Any] | None: for entry in self._ranking: if entry["method_name"] == function_to_optimize.function_name: return { diff --git a/codeflash/languages/java/jfr_parser.py b/codeflash/languages/java/jfr_parser.py index c44e8f46b..c5c368e99 100644 --- a/codeflash/languages/java/jfr_parser.py +++ b/codeflash/languages/java/jfr_parser.py @@ -5,6 +5,7 @@ import shutil import subprocess from pathlib import Path +from typing import Any logger = logging.getLogger(__name__) @@ -126,7 +127,7 @@ def _parse_json(self, json_str: str) -> None: if len(timestamps) >= 2: self._recording_duration_ns = max(timestamps) - min(timestamps) - def _frame_to_key(self, frame: dict) -> str | None: + def _frame_to_key(self, frame: dict[str, Any]) -> str | None: method = frame.get("method", {}) class_name = method.get("type", {}).get("name", "") method_name = method.get("name", "") @@ -134,7 +135,7 @@ def _frame_to_key(self, frame: dict) -> str | None: return None return f"{class_name}.{method_name}" - def _store_method_info(self, key: str, frame: dict) -> None: + def _store_method_info(self, key: str, frame: dict[str, Any]) -> None: if key in self._method_info: return method = frame.get("method", {}) @@ -150,7 +151,7 @@ def _matches_packages(self, method_key: str) -> bool: return True return any(method_key.startswith(pkg) for pkg in self.packages) - def get_method_ranking(self) -> list[dict]: + def get_method_ranking(self) -> list[dict[str, Any]]: if not self._method_samples or self._total_samples == 0: return [] diff --git a/codeflash/tracer.py b/codeflash/tracer.py index dbc5a5a04..5293c971d 100644 --- a/codeflash/tracer.py +++ b/codeflash/tracer.py @@ -32,15 +32,17 @@ if TYPE_CHECKING: from argparse import Namespace + from codeflash.languages import Language + logger = logging.getLogger(__name__) -def _detect_non_python_language(args: Namespace | None) -> object | None: +def _detect_non_python_language(args: Namespace | None) -> Language | None: """Detect if the project uses a non-Python language from --file or config. Returns a Language enum value if non-Python detected, None otherwise. """ - from codeflash.languages.base import Language + from codeflash.languages import Language # Method 1: Check --file argument for non-Python file extension file_path_to_check: Path | None = None @@ -91,7 +93,7 @@ def main(args: Namespace | None = None) -> ArgumentParser: # 2. language field in project config (codeflash.toml or pyproject.toml) detected_language = _detect_non_python_language(args) if detected_language is not None: - from codeflash.languages.base import Language + from codeflash.languages import Language if detected_language in (Language.JAVASCRIPT, Language.TYPESCRIPT): from codeflash.cli_cmds.cli import parse_args, process_pyproject_config @@ -179,16 +181,18 @@ def main(args: Namespace | None = None) -> ArgumentParser: "module": parsed_args.module, } try: - pytest_splits = [] - test_paths = [] - replay_test_paths = [] + pytest_splits: list[list[str]] = [] + test_paths: list[str] = [] + replay_test_paths: list[str] = [] if parsed_args.module and unknown_args[0] == "pytest": - pytest_splits, test_paths = pytest_split(unknown_args[1:], limit=parsed_args.limit) - if pytest_splits is None or test_paths is None: + result_splits, result_paths = pytest_split(unknown_args[1:], limit=parsed_args.limit) + if result_splits is None or result_paths is None: console.print(f"❌ Could not find test files in the specified paths: {unknown_args[1:]}") console.print(f"Current working directory: {Path.cwd()}") console.print("Please ensure the test directory exists and contains test files.") sys.exit(1) + pytest_splits = result_splits + test_paths = result_paths if len(pytest_splits) > 1: processes = [] @@ -276,7 +280,7 @@ def main(args: Namespace | None = None) -> ArgumentParser: from codeflash.cli_cmds.console import paneled_text from codeflash.cli_cmds.console_constants import CODEFLASH_LOGO from codeflash.languages import set_current_language - from codeflash.languages.base import Language + from codeflash.languages import Language from codeflash.telemetry import posthog_cf from codeflash.telemetry.sentry import init_sentry @@ -375,7 +379,7 @@ def _run_java_tracer(existing_args: Namespace | None = None) -> ArgumentParser: if not trace_only and test_count > 0: from codeflash.code_utils.config_consts import EffortLevel from codeflash.languages import set_current_language - from codeflash.languages.base import Language + from codeflash.languages import Language from codeflash.optimization import optimizer set_current_language(Language.JAVA) From d12e631ce935b861a956c1a7b3f95495d0cd114f Mon Sep 17 00:00:00 2001 From: misrasaurabh1 Date: Thu, 19 Mar 2026 00:44:29 -0700 Subject: [PATCH 08/12] add some initial java docs --- docs/configuration/java.mdx | 153 ++++++++++++++++++ docs/docs.json | 4 +- docs/getting-started/java-installation.mdx | 131 +++++++++++++++ .../trace-and-optimize.mdx | 76 +++++++++ 4 files changed, 363 insertions(+), 1 deletion(-) create mode 100644 docs/configuration/java.mdx create mode 100644 docs/getting-started/java-installation.mdx diff --git a/docs/configuration/java.mdx b/docs/configuration/java.mdx new file mode 100644 index 000000000..9d110fc55 --- /dev/null +++ b/docs/configuration/java.mdx @@ -0,0 +1,153 @@ +--- +title: "Java Configuration" +description: "Configure Codeflash for Java projects using codeflash.toml" +icon: "java" +sidebarTitle: "Java (codeflash.toml)" +keywords: + [ + "configuration", + "codeflash.toml", + "java", + "maven", + "gradle", + "junit", + ] +--- + +# Java Configuration + +Codeflash stores its configuration in `codeflash.toml` under the `[tool.codeflash]` section. + +## Full Reference + +```toml +[tool.codeflash] +# Required +module-root = "src/main/java" +tests-root = "src/test/java" +language = "java" + +# Optional +test-framework = "junit5" # "junit5", "junit4", or "testng" +disable-telemetry = false +git-remote = "origin" +ignore-paths = ["src/main/java/generated/"] +``` + +All file paths are relative to the directory containing `codeflash.toml`. + + +Codeflash auto-detects most settings from your project structure. Running `codeflash init` will set up the correct config — manual configuration is usually not needed. + + +## Auto-Detection + +When you run `codeflash init`, Codeflash inspects your project and auto-detects: + +| Setting | Detection logic | +|---------|----------------| +| `module-root` | Looks for `src/main/java` (Maven/Gradle standard layout) | +| `tests-root` | Looks for `src/test/java`, `test/`, `tests/` | +| `language` | Detected from build files (`pom.xml`, `build.gradle`) and `.java` files | +| `test-framework` | Checks build file dependencies for JUnit 5, JUnit 4, or TestNG | + +## Required Options + +- **`module-root`**: The source directory to optimize. Only code under this directory is discovered for optimization. For standard Maven/Gradle projects, this is `src/main/java`. +- **`tests-root`**: The directory where your tests are located. Codeflash discovers existing tests and places generated replay tests here. +- **`language`**: Must be set to `"java"` for Java projects. + +## Optional Options + +- **`test-framework`**: Test framework. Auto-detected from build dependencies. Supported values: `"junit5"` (default), `"junit4"`, `"testng"`. +- **`disable-telemetry`**: Disable anonymized telemetry. Defaults to `false`. +- **`git-remote`**: Git remote for pull requests. Defaults to `"origin"`. +- **`ignore-paths`**: Paths within `module-root` to skip during optimization. + +## Multi-Module Projects + +For multi-module Maven/Gradle projects, place `codeflash.toml` at the project root and set `module-root` to the module you want to optimize: + +```text +my-project/ +|- client/ +| |- src/main/java/com/example/client/ +| |- src/test/java/com/example/client/ +|- server/ +| |- src/main/java/com/example/server/ +|- pom.xml +|- codeflash.toml +``` + +```toml +[tool.codeflash] +module-root = "client/src/main/java" +tests-root = "client/src/test/java" +language = "java" +``` + +For non-standard layouts (like the Aerospike client where source is under `client/src/`), adjust paths accordingly: + +```toml +[tool.codeflash] +module-root = "client/src" +tests-root = "test/src" +language = "java" +``` + +## Tracer Options + +When using `codeflash optimize` to trace a Java program, these CLI options are available: + +| Option | Description | Default | +|--------|------------|---------| +| `--timeout` | Maximum time (seconds) for each tracing stage | No limit | +| `--max-function-count` | Maximum captures per method | 100 | +| `--trace-only` | Trace and generate replay tests without optimizing | `false` | + +Example with timeout: + +```bash +codeflash optimize --timeout 30 java -jar target/my-app.jar --app-args +``` + +## Example + +### Standard Maven project + +```text +my-app/ +|- src/ +| |- main/java/com/example/ +| | |- App.java +| | |- Utils.java +| |- test/java/com/example/ +| |- AppTest.java +|- pom.xml +|- codeflash.toml +``` + +```toml +[tool.codeflash] +module-root = "src/main/java" +tests-root = "src/test/java" +language = "java" +``` + +### Gradle project + +```text +my-lib/ +|- src/ +| |- main/java/com/example/ +| |- test/java/com/example/ +|- build.gradle +|- codeflash.toml +``` + +```toml +[tool.codeflash] +module-root = "src/main/java" +tests-root = "src/test/java" +language = "java" +``` diff --git a/docs/docs.json b/docs/docs.json index fe0c23098..e3fead77f 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -26,7 +26,8 @@ "group": "Getting Started", "pages": [ "getting-started/local-installation", - "getting-started/javascript-installation" + "getting-started/javascript-installation", + "getting-started/java-installation" ] }, { @@ -45,6 +46,7 @@ "pages": [ "configuration/python", "configuration/javascript", + "configuration/java", "getting-the-best-out-of-codeflash" ] }, diff --git a/docs/getting-started/java-installation.mdx b/docs/getting-started/java-installation.mdx new file mode 100644 index 000000000..a75e1f0b7 --- /dev/null +++ b/docs/getting-started/java-installation.mdx @@ -0,0 +1,131 @@ +--- +title: "Java Installation" +description: "Install and configure Codeflash for your Java project" +icon: "java" +sidebarTitle: "Java Setup" +keywords: + [ + "installation", + "java", + "maven", + "gradle", + "junit", + "junit5", + "tracing", + ] +--- + +Codeflash supports Java projects using Maven or Gradle build systems. It uses a two-stage tracing approach to capture method arguments and profiling data from running Java programs, then optimizes the hottest functions. + +### Prerequisites + +Before installing Codeflash, ensure you have: + +1. **Java 11 or above** installed +2. **Maven or Gradle** as your build tool +3. **A Java project** with source code under a standard directory layout + +Good to have (optional): + +1. **Unit tests** (JUnit 5 or JUnit 4) — Codeflash uses them alongside traced replay tests to verify correctness + + + + +Codeflash CLI is a Python tool. Install it with pip: + +```bash +pip install codeflash +``` + +Or with uv: + +```bash +uv pip install codeflash +``` + + + + +Navigate to your Java project root (where `pom.xml` or `build.gradle` is) and run: + +```bash +codeflash init +``` + +This will: +- Detect your build tool (Maven/Gradle) +- Find your source and test directories +- Create a `codeflash.toml` configuration file + + + + +Check that the configuration looks correct: + +```bash +cat codeflash.toml +``` + +You should see something like: + +```toml +[tool.codeflash] +module-root = "src/main/java" +tests-root = "src/test/java" +language = "java" +``` + + + + +Trace and optimize a running Java program: + +```bash +codeflash optimize java -jar target/my-app.jar +``` + +Or with Maven: + +```bash +codeflash optimize mvn exec:java -Dexec.mainClass="com.example.Main" +``` + +Codeflash will: +1. Profile your program using JFR (Java Flight Recorder) +2. Capture method arguments using a bytecode instrumentation agent +3. Generate JUnit replay tests from the captured data +4. Rank functions by performance impact +5. Optimize the most impactful functions + + + + +## How it works + +Codeflash uses a **two-stage tracing** approach for Java: + +1. **Stage 1 — JFR Profiling**: Runs your program with Java Flight Recorder enabled to collect accurate method-level CPU profiling data. JFR has ~1% overhead and doesn't affect JIT compilation. + +2. **Stage 2 — Argument Capture**: Runs your program again with a bytecode instrumentation agent that captures method arguments using Kryo serialization. Arguments are stored in an SQLite database. + +The traced data is used to generate **JUnit replay tests** that exercise your functions with real-world inputs. Codeflash uses these tests alongside any existing unit tests to verify correctness and benchmark optimization candidates. + + +Your program runs **twice** — once for profiling, once for argument capture. This separation ensures profiling data isn't distorted by serialization overhead. + + +## Supported build tools + +| Build Tool | Detection | Test Execution | +|-----------|-----------|---------------| +| **Maven** | `pom.xml` | Maven Surefire plugin | +| **Gradle** | `build.gradle` / `build.gradle.kts` | Gradle test task | + +## Supported test frameworks + +| Framework | Support Level | +|-----------|-------------| +| **JUnit 5** | Full support (default) | +| **JUnit 4** | Full support | +| **TestNG** | Basic support | diff --git a/docs/optimizing-with-codeflash/trace-and-optimize.mdx b/docs/optimizing-with-codeflash/trace-and-optimize.mdx index fb62ea1c1..4c332a929 100644 --- a/docs/optimizing-with-codeflash/trace-and-optimize.mdx +++ b/docs/optimizing-with-codeflash/trace-and-optimize.mdx @@ -15,6 +15,10 @@ keywords: "typescript", "jest", "vitest", + "java", + "jfr", + "maven", + "gradle", ] --- @@ -52,6 +56,26 @@ codeflash optimize --vitest codeflash optimize --language javascript script.js ``` + +To trace and optimize a running Java program, replace your `java` command with `codeflash optimize java`: + +```bash +# JAR application +codeflash optimize java -jar target/my-app.jar --app-args + +# Class with classpath +codeflash optimize java -cp target/classes com.example.Main + +# Maven exec +codeflash optimize mvn exec:java -Dexec.mainClass="com.example.Main" +``` + +For long-running programs (servers, benchmarks), use `--timeout` to limit each tracing stage: + +```bash +codeflash optimize --timeout 30 java -jar target/my-app.jar +``` + The `codeflash optimize` command creates high-quality optimizations, making it ideal when you need to optimize a workflow or script. The initial tracing process can be slow, so try to limit your script's runtime to under 1 minute for best results. @@ -194,5 +218,57 @@ The JavaScript tracer uses Babel instrumentation to capture function calls durin - `--max-function-count`: Maximum traces per function (default: 256). - `--only-functions`: Comma-separated list of function names to trace. + + + +The Java tracer uses a **two-stage approach**: JFR (Java Flight Recorder) for accurate profiling, then a bytecode instrumentation agent for argument capture. + +1. **Trace and optimize a Java program** + + Replace your `java` command with `codeflash optimize java`: + + ```bash + # JAR application + codeflash optimize java -jar target/my-app.jar --app-args + + # Class with classpath + codeflash optimize java -cp target/classes com.example.Main + ``` + + Codeflash will run your program twice (once for profiling, once for argument capture), generate JUnit replay tests, then optimize the most impactful functions. + +2. **Long-running programs** + + For servers, benchmarks, or programs that don't terminate on their own, use `--timeout` to limit each tracing stage: + + ```bash + codeflash optimize --timeout 30 java -jar target/my-benchmark.jar + ``` + + Each stage runs for at most 30 seconds, then the program is terminated and captured data is processed. + +3. **Trace only (no optimization)** + + ```bash + codeflash optimize --trace-only java -jar target/my-app.jar + ``` + + This generates replay tests in `src/test/java/codeflash/replay/` without running the optimizer. + + More Options: + + - `--timeout`: Maximum time (seconds) for each tracing stage. + - `--max-function-count`: Maximum captures per method (default: 100). + + +**How the Java tracer works:** + +- **Stage 1 (JFR)**: Runs your program with Java Flight Recorder enabled. JFR is built into the JVM (Java 11+), has ~1% overhead, and doesn't interfere with JIT compilation. This produces accurate method-level CPU profiling data. + +- **Stage 2 (Agent)**: Runs your program with a bytecode instrumentation agent injected via `JAVA_TOOL_OPTIONS`. The agent intercepts method entry points, serializes arguments using Kryo, and writes them to an SQLite database. A 500ms timeout per serialization prevents hangs on complex object graphs. + +- **Replay Tests**: Generated JUnit 5 test classes that deserialize captured arguments and invoke the original methods via reflection. These tests exercise your code with real-world inputs. + + From 0cf1d1b951807c209bc8a91079fed64ea4e8cc1b Mon Sep 17 00:00:00 2001 From: "codeflash-ai[bot]" <148906541+codeflash-ai[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 08:11:56 +0000 Subject: [PATCH 09/12] Optimize JfrProfile._parse_json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The optimization precomputes all frame-to-key conversions for a stack trace once (into a `keys` list) instead of calling `_frame_to_key` repeatedly inside the caller-callee loop, cutting per-frame extraction from ~3.3 µs to ~0.19 µs (83% reduction) and lifting `_frame_to_key` from 20.8% of total time to 43.2% (the loop cost is now dominated by the upfront list comprehension rather than repeated calls). A local `matches_packages_cached` closure memoizes package-filter results to avoid re-checking the same method keys across caller relationships, reducing `_matches_packages` overhead from 12.6% to 0.8% of total time; profiler data shows `_matches_packages` hits dropped from 18,364 to 1,500. The timestamp-duration calculation switched from accumulating a list then calling `max()`/`min()` to inline min/max tracking, removing intermediate allocations; combined, these changes yield a 42% overall speedup (46.4 ms → 32.6 ms). --- codeflash/languages/java/jfr_parser.py | 65 +++++++++++++++++--------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/codeflash/languages/java/jfr_parser.py b/codeflash/languages/java/jfr_parser.py index c5c368e99..7775378e6 100644 --- a/codeflash/languages/java/jfr_parser.py +++ b/codeflash/languages/java/jfr_parser.py @@ -4,6 +4,7 @@ import logging import shutil import subprocess +from datetime import datetime from pathlib import Path from typing import Any @@ -81,6 +82,16 @@ def _parse_json(self, json_str: str) -> None: if not events: events = data.get("events", []) + # Cache package matching results to avoid repeated checks + package_match_cache: dict[str, bool] = {} + + def matches_packages_cached(method_key: str | None) -> bool: + if method_key is None: + return False + if method_key not in package_match_cache: + package_match_cache[method_key] = self._matches_packages(method_key) + return package_match_cache[method_key] + for event in events: if event.get("type") != "jdk.ExecutionSample": continue @@ -92,40 +103,48 @@ def _parse_json(self, json_str: str) -> None: self._total_samples += 1 + # Precompute keys for all frames in this stack to avoid repeated conversions + keys = [self._frame_to_key(f) for f in frames] + # Top-of-stack = own time - top_frame = frames[0] - top_method_key = self._frame_to_key(top_frame) - if top_method_key and self._matches_packages(top_method_key): + top_method_key = keys[0] if keys else None + if matches_packages_cached(top_method_key): self._method_samples[top_method_key] = self._method_samples.get(top_method_key, 0) + 1 - self._store_method_info(top_method_key, top_frame) + self._store_method_info(top_method_key, frames[0]) # Build caller-callee relationships from adjacent frames - for i in range(len(frames) - 1): - callee_key = self._frame_to_key(frames[i]) - caller_key = self._frame_to_key(frames[i + 1]) - if callee_key and caller_key and self._matches_packages(callee_key): + for i in range(len(keys) - 1): + callee_key = keys[i] + caller_key = keys[i + 1] + if callee_key and caller_key and matches_packages_cached(callee_key): callee_callers = self._caller_map.setdefault(callee_key, {}) callee_callers[caller_key] = callee_callers.get(caller_key, 0) + 1 # Estimate recording duration from event timestamps if events: - timestamps = [] + min_ts = None + max_ts = None for event in events: - start_time = event.get("values", {}).get("startTime") - if start_time: - try: - # JFR timestamps are in ISO format or epoch nanos - if isinstance(start_time, str): - from datetime import datetime - - dt = datetime.fromisoformat(start_time.replace("Z", "+00:00")) - timestamps.append(int(dt.timestamp() * 1_000_000_000)) - elif isinstance(start_time, (int, float)): - timestamps.append(int(start_time)) - except (ValueError, TypeError): + try: + start_time = event.get("values", {}).get("startTime") + if not start_time: + continue + # JFR timestamps are in ISO format or epoch nanos + if isinstance(start_time, str): + dt = datetime.fromisoformat(start_time.replace("Z", "+00:00")) + ts = int(dt.timestamp() * 1_000_000_000) + elif isinstance(start_time, (int, float)): + ts = int(start_time) + else: continue - if len(timestamps) >= 2: - self._recording_duration_ns = max(timestamps) - min(timestamps) + if min_ts is None or ts < min_ts: + min_ts = ts + if max_ts is None or ts > max_ts: + max_ts = ts + except (ValueError, TypeError): + continue + if min_ts is not None and max_ts is not None: + self._recording_duration_ns = max_ts - min_ts def _frame_to_key(self, frame: dict[str, Any]) -> str | None: method = frame.get("method", {}) From fa1ed76e88747e311682696fc1d6963a2e4367ae Mon Sep 17 00:00:00 2001 From: misrasaurabh1 Date: Thu, 19 Mar 2026 11:37:15 -0700 Subject: [PATCH 10/12] =?UTF-8?q?fix:=20address=20review=20feedback=20?= =?UTF-8?q?=E2=80=94=20dedup=20parser,=20error=20on=20missing=20command,?= =?UTF-8?q?=20filter=20empty=20names?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Consolidate _parse_replay_metadata to call parse_replay_test_metadata instead of duplicating the parsing logic - Replace hardcoded fallback java command with a clear error message when no java command is provided - Filter empty strings from function_names split (\"".split(\",\") returns [\"\"] which is truthy) - Fix import ordering in tracer.py (ruff I001) Co-Authored-By: Claude Opus 4.6 (1M context) --- codeflash/discovery/functions_to_optimize.py | 2 +- codeflash/languages/java/test_discovery.py | 20 ++++---------------- codeflash/tracer.py | 13 ++++++++----- package-lock.json | 0 package.json | 0 5 files changed, 13 insertions(+), 22 deletions(-) create mode 100644 package-lock.json create mode 100644 package.json diff --git a/codeflash/discovery/functions_to_optimize.py b/codeflash/discovery/functions_to_optimize.py index 607039273..5780f4def 100644 --- a/codeflash/discovery/functions_to_optimize.py +++ b/codeflash/discovery/functions_to_optimize.py @@ -569,7 +569,7 @@ def _get_java_replay_test_functions( trace_file_path = Path(metadata["trace_file"]) classname = metadata.get("classname", "") - function_names = metadata.get("functions", "").split(",") + function_names = [f.strip() for f in metadata.get("functions", "").split(",") if f.strip()] if not classname or not function_names: continue diff --git a/codeflash/languages/java/test_discovery.py b/codeflash/languages/java/test_discovery.py index 89dbe420a..7db04298b 100644 --- a/codeflash/languages/java/test_discovery.py +++ b/codeflash/languages/java/test_discovery.py @@ -120,22 +120,10 @@ def _parse_replay_metadata(test_file: Path) -> dict[str, str] | None: Returns metadata dict if it's a replay test, None otherwise. """ - try: - metadata: dict[str, str] = {} - with test_file.open("r", encoding="utf-8") as f: - for line in f: - stripped = line.strip() - if not stripped.startswith("// codeflash:"): - if stripped and not stripped.startswith("//"): - break - continue - key_value = stripped[len("// codeflash:") :] - if "=" in key_value: - key, value = key_value.split("=", 1) - metadata[key] = value - return metadata if "functions" in metadata else None - except Exception: - return None + from codeflash.languages.java.replay_test import parse_replay_test_metadata + + metadata = parse_replay_test_metadata(test_file) + return metadata if "functions" in metadata else None def _discover_replay_tests( diff --git a/codeflash/tracer.py b/codeflash/tracer.py index 5293c971d..84f58e9da 100644 --- a/codeflash/tracer.py +++ b/codeflash/tracer.py @@ -279,8 +279,7 @@ def main(args: Namespace | None = None) -> ArgumentParser: from codeflash.cli_cmds.cli import parse_args, process_pyproject_config from codeflash.cli_cmds.console import paneled_text from codeflash.cli_cmds.console_constants import CODEFLASH_LOGO - from codeflash.languages import set_current_language - from codeflash.languages import Language + from codeflash.languages import Language, set_current_language from codeflash.telemetry import posthog_cf from codeflash.telemetry.sentry import init_sentry @@ -358,7 +357,12 @@ def _run_java_tracer(existing_args: Namespace | None = None) -> ArgumentParser: # Remaining args after our flags are the Java command remaining = sys.argv[sys.argv.index("--file") + 2 :] if "--file" in sys.argv else sys.argv[1:] - java_command = remaining if remaining else ["java", "-jar", "app.jar"] + if not remaining: + console.print("[bold red]Error:[/] No Java command provided.") + console.print("Usage: codeflash optimize java -jar target/my-app.jar [args...]") + console.print(" codeflash optimize java -cp target/classes com.example.Main [args...]") + sys.exit(1) + java_command = remaining trace_db, jfr_file, test_count = run_java_tracer( java_command=java_command, @@ -378,8 +382,7 @@ def _run_java_tracer(existing_args: Namespace | None = None) -> ArgumentParser: if not trace_only and test_count > 0: from codeflash.code_utils.config_consts import EffortLevel - from codeflash.languages import set_current_language - from codeflash.languages import Language + from codeflash.languages import Language, set_current_language from codeflash.optimization import optimizer set_current_language(Language.JAVA) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..e69de29bb diff --git a/package.json b/package.json new file mode 100644 index 000000000..e69de29bb From 3bf8ffb76a0c10d6e91db923c8992ec04249f8e0 Mon Sep 17 00:00:00 2001 From: misrasaurabh1 Date: Thu, 19 Mar 2026 11:47:53 -0700 Subject: [PATCH 11/12] chore: remove accidental package-lock.json and package.json These files were unrelated to the PR and got swept in during a stash/pop operation. Co-Authored-By: Claude Opus 4.6 (1M context) --- package-lock.json | 0 package.json | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 package-lock.json delete mode 100644 package.json diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index e69de29bb..000000000 diff --git a/package.json b/package.json deleted file mode 100644 index e69de29bb..000000000 From 3b396578d74b017acbcb5d90792bb23553a33da5 Mon Sep 17 00:00:00 2001 From: misrasaurabh1 Date: Thu, 19 Mar 2026 12:22:26 -0700 Subject: [PATCH 12/12] make workload purposefully terrible --- .../src/main/java/com/example/Workload.java | 48 +++++-------------- 1 file changed, 12 insertions(+), 36 deletions(-) diff --git a/tests/test_languages/fixtures/java_tracer_e2e/src/main/java/com/example/Workload.java b/tests/test_languages/fixtures/java_tracer_e2e/src/main/java/com/example/Workload.java index b73f886ac..9b6078000 100644 --- a/tests/test_languages/fixtures/java_tracer_e2e/src/main/java/com/example/Workload.java +++ b/tests/test_languages/fixtures/java_tracer_e2e/src/main/java/com/example/Workload.java @@ -6,57 +6,33 @@ public class Workload { public static int computeSum(int n) { - if (n <= 0) { - return 0; + int sum = 0; + for (int i = 0; i < n; i++) { + sum += i; } - long nl = n; - long result = (nl * (nl - 1)) / 2; - return (int) result; + return sum; } public static String repeatString(String s, int count) { - StringBuilder sb = new StringBuilder(); + String result = ""; for (int i = 0; i < count; i++) { - sb.append(s); + result = result + s; } - return sb.toString(); + return result; } public static List filterEvens(List numbers) { - // Pre-size result to avoid repeated resizes. Keep same behavior for null input (will NPE). - List result = new ArrayList<>(numbers.size()); - - // Use indexed loop for RandomAccess lists (e.g., ArrayList) to avoid iterator allocation, - // but fall back to iterator-based (enhanced for) loop for non-random-access lists - // to prevent O(n^2) behavior on LinkedList. - if (numbers instanceof java.util.RandomAccess) { - for (int i = 0, sz = numbers.size(); i < sz; i++) { - Integer num = numbers.get(i); - if ((num & 1) == 0) { // faster parity check than modulus - result.add(num); - } - } - } else { - for (Integer n : numbers) { - if ((n & 1) == 0) { - result.add(n); - } + List result = new ArrayList<>(); + for (int n : numbers) { + if (n % 2 == 0) { + result.add(n); } } return result; } public int instanceMethod(int x, int y) { - // Inline computeSum logic to avoid the static method call overhead on the hot path. - int sum; - if (x <= 0) { - sum = 0; - } else { - long nl = x; - long result = (nl * (nl - 1)) >> 1; - sum = (int) result; - } - return x * y + sum; + return x * y + computeSum(x); } public static void main(String[] args) {