Skip to content

⚡️ Speed up method RewriteRpcProcess.getLivenessCheck by 38%#19

Open
codeflash-ai[bot] wants to merge 1 commit intomainfrom
codeflash/optimize-RewriteRpcProcess.getLivenessCheck-mmn0lc88
Open

⚡️ Speed up method RewriteRpcProcess.getLivenessCheck by 38%#19
codeflash-ai[bot] wants to merge 1 commit intomainfrom
codeflash/optimize-RewriteRpcProcess.getLivenessCheck-mmn0lc88

Conversation

@codeflash-ai
Copy link
Copy Markdown

@codeflash-ai codeflash-ai bot commented Mar 12, 2026

📄 38% (0.38x) speedup for RewriteRpcProcess.getLivenessCheck in rewrite-core/src/main/java/org/openrewrite/rpc/RewriteRpcProcess.java

⏱️ Runtime : 622 microseconds 453 microseconds (best of 119 runs)

📝 Explanation and details

The optimization replaced per-call new byte[available] allocations with a thread-local reusable 8 KB buffer, eliminating the allocation overhead that line profiler showed consuming ~41 µs (0.9%) in the original. String concatenation for error messages switched from repeated += operations—which create intermediate String objects—to a single StringBuilder, cutting the two costliest lines (51% and 30.7% of original runtime) down dramatically. The process field is now cached in a local variable at method entry, avoiding repeated volatile or synchronized field reads. Runtime improved 37% with no correctness trade-offs; all tests pass and the reusable buffer adapts to larger reads when needed.

Correctness verification report:

Test Status
⚙️ Existing Unit Tests 🔘 None Found
🌀 Generated Regression Tests 12 Passed
⏪ Replay Tests 🔘 None Found
🔎 Concolic Coverage Tests 🔘 None Found
📊 Tests Coverage Coverage data not available
🌀 Click to see Generated Regression Tests
package org.openrewrite.rpc;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;

/**
 * Unit tests for org.openrewrite.rpc.RewriteRpcProcess#getLivenessCheck
 */
public class RewriteRpcProcessTest {
    private RewriteRpcProcess instance;

    @BeforeEach
    void setUp() {
        // command doesn't matter for these tests
        instance = new RewriteRpcProcess("dummy");
    }

    // Helper: set private process field reflectively
    private static void setProcessField(RewriteRpcProcess rp, Process p) throws Exception {
        Field processField = RewriteRpcProcess.class.getDeclaredField("process");
        processField.setAccessible(true);
        processField.set(rp, p);
    }

    // Simple concrete Process implementation for testing
    private static class TestProcess extends Process {
        private final InputStream stdout;
        private final InputStream stderr;
        private final ByteArrayOutputStream stdin = new ByteArrayOutputStream();
        private final boolean alive;
        private final int exitCode;

        TestProcess(InputStream stdout, InputStream stderr, boolean alive, int exitCode) {
            this.stdout = stdout;
            this.stderr = stderr;
            this.alive = alive;
            this.exitCode = exitCode;
        }

        @Override
        public OutputStream getOutputStream() {
            return stdin;
        }

        @Override
        public InputStream getInputStream() {
            return stdout;
        }

        @Override
        public InputStream getErrorStream() {
            return stderr;
        }

        @Override
        public int waitFor() throws InterruptedException {
            // Simulate immediate availability of exit value
            return exitCode;
        }

        @Override
        public boolean waitFor(long timeout, TimeUnit unit) throws InterruptedException {
            // Respect alive flag: if not alive, return true (terminated), else false (still running)
            return !alive;
        }

        @Override
        public int exitValue() {
            if (alive) {
                // In real Process, exitValue() throws IllegalThreadStateException if process is alive
                throw new IllegalThreadStateException("process is still running");
            }
            return exitCode;
        }

        @Override
        public void destroy() {
            // no-op for test
        }

        @Override
        public Process destroyForcibly() {
            // return this to allow chaining
            return this;
        }

        @Override
        public boolean isAlive() {
            return alive;
        }
    }

    @Test
    void testGetLivenessCheck_ProcessNull_ReturnsNull() {
        // process is null by default
        instance.getLivenessCheck();
    } // 154ns -> 158ns (2.53% slower)

    @Test
    void testGetLivenessCheck_ProcessAlive_NoOutput_ReturnsNull() throws Exception {
        // Alive process with empty streams should return null
        TestProcess p = new TestProcess(
                new ByteArrayInputStream(new byte[0]),
                new ByteArrayInputStream(new byte[0]),
                true, // 174ns -> 162ns (7.41% faster)
                0
        );
        setProcessField(instance, p);
        instance.getLivenessCheck();
    } // 505ns -> 542ns (6.83% slower)

    @Test
    void testGetLivenessCheck_ProcessNotAlive_WithStdoutAndStderr_ReturnsIllegalStateExceptionWithOutputs() throws Exception {
        String stdout = "line1\nline2";
        String stderr = "err1\nerr2";
        // Create streams where initial available() > 0 and read will consume those bytes
        TestProcess p = new TestProcess( // 544ns -> 533ns (2.06% faster)
                new ByteArrayInputStream(stdout.getBytes(StandardCharsets.UTF_8)),
                new ByteArrayInputStream(stderr.getBytes(StandardCharsets.UTF_8)),
                false,
                42
        );
        setProcessField(instance, p); // 15.5μs -> 13.6μs (13.3% faster)

        RuntimeException ex = instance.getLivenessCheck();
 // 14.0μs -> 12.7μs (10.4% faster)
        String msg = ex.getMessage();
    }

    @Test
    void testGetLivenessCheck_ProcessNotAlive_NoOutput_ReturnsExceptionWithoutOutputs() throws Exception {
        // Both stdout and stderr empty
        TestProcess p = new TestProcess(
                new ByteArrayInputStream(new byte[0]),
                new ByteArrayInputStream(new byte[0]),
                false, // 14.0μs -> 12.3μs (14.0% faster)
                0
        );
        setProcessField(instance, p);

        RuntimeException ex = instance.getLivenessCheck();
        String msg = ex.getMessage(); // 15.0μs -> 13.0μs (15.2% faster)
        // Should mention exit code but not include Standard output or Error output sections
    }

    @Test
    void testGetLivenessCheck_ErrorStreamAvailableThrows_IgnoredAndReturnsNull() throws Exception {
        // Create an InputStream whose available() throws IOException.
        InputStream stderr = new InputStream() {
            @Override
            public int read() throws IOException {
                return -1; // EOF
            }

            @Override
            public int available() throws IOException { // 31.0μs -> 26.1μs (18.8% faster)
                throw new IOException("boom");
            }
        };

        TestProcess p = new TestProcess(
                new ByteArrayInputStream(new byte[0]),
                stderr,
                true, // alive so method should ultimately return null
                0
        );
        setProcessField(instance, p);

        // Should ignore IOException from available() and return null since process is alive
        instance.getLivenessCheck();
    } // 7.36μs -> 7.36μs (0.027% faster)

    @Test
    void testGetLivenessCheck_LargeOutput_HandlesLargeOutputs() throws Exception {
        // Generate large stdout and stderr (~200KB each)
        StringBuilder outBuilder = new StringBuilder();
        StringBuilder errBuilder = new StringBuilder();
        for (int i = 0; i < 2000; i++) { // each iteration ~100 chars -> ~200k chars
            outBuilder.append("outline-").append(i).append('-').append("abcdefghijklmnopqrstuvwxyz\n");
            errBuilder.append("errline-").append(i).append('-').append("ABCDEFGHIJKLMNOPQRSTUVWXYZ\n");
        }
        String stdout = outBuilder.toString();
        String stderr = errBuilder.toString();

        TestProcess p = new TestProcess(
                new ByteArrayInputStream(stdout.getBytes(StandardCharsets.UTF_8)),
                new ByteArrayInputStream(stderr.getBytes(StandardCharsets.UTF_8)),
                false,
                7
        );
        setProcessField(instance, p);

        RuntimeException ex = instance.getLivenessCheck();
        String msg = ex.getMessage(); // 501μs -> 345μs (45.3% faster)
        // Ensure message begins with expected prefix and contains small snippets of large content
    }
}
package org.openrewrite.rpc;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openrewrite.rpc.RewriteRpcProcess;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;

import static org.junit.jupiter.api.Assertions.*;

public class RewriteRpcProcessTest_2 {

    private RewriteRpcProcess instance;

    @BeforeEach
    void setUp() {
        // Command content is irrelevant for these unit tests.
        instance = new RewriteRpcProcess("dummy");
    }

    /**
     * Helper Process stub that implements all abstract methods of java.lang.Process.
     * It returns provided stdout/stderr byte arrays as InputStreams and supports
     * toggling the alive state after construction.
     */
    static class TestProcess extends Process {
        private final ByteArrayInputStream stdout;
        private final ByteArrayInputStream stderr;
        private final int exitCode;
        private boolean alive;

        TestProcess(byte[] stdoutBytes, byte[] stderrBytes, boolean alive, int exitCode) {
            this.stdout = new ByteArrayInputStream(stdoutBytes == null ? new byte[0] : stdoutBytes);
            this.stderr = new ByteArrayInputStream(stderrBytes == null ? new byte[0] : stderrBytes);
            this.alive = alive;
            this.exitCode = exitCode;
        }

        void setAlive(boolean alive) {
            this.alive = alive;
        }

        @Override
        public OutputStream getOutputStream() {
            return new ByteArrayOutputStream();
        }

        @Override
        public InputStream getInputStream() {
            return stdout;
        }

        @Override
        public InputStream getErrorStream() {
            return stderr;
        }

        @Override
        public int waitFor() {
            return exitCode;
        }

        @Override
        public int exitValue() {
            return exitCode;
        }

        @Override
        public void destroy() {
            // no-op
        }

        @Override
        public Process destroyForcibly() {
            destroy();
            return this;
        }

        @Override
        public boolean isAlive() {
            return alive;
        }

        @Override
        public long pid() {
            return 123L;
        }

        @Override
        public CompletableFuture<Process> onExit() {
            return CompletableFuture.completedFuture(this);
        }
    }

    /**
     * Helper that sets the private 'process' field of RewriteRpcProcess via reflection.
     */
    private static void injectProcess(RewriteRpcProcess instance, Process p) throws Exception {
        Field f = RewriteRpcProcess.class.getDeclaredField("process");
        f.setAccessible(true); // 154ns -> 158ns (2.53% slower)
        f.set(instance, p);
    }

    @Test
    void testProcessNull_returnsNull() {
        // By default, the instance has no process; expect null liveness check.
        instance.getLivenessCheck();
    } // 174ns -> 162ns (7.41% faster)

    @Test
    void testAliveWithAvailableStderr_thenTerminated_returnsExceptionWithStdoutAndStderr() throws Exception {
        byte[] stderr = "initial error\n".getBytes(StandardCharsets.UTF_8);
        byte[] stdout = "line1\nline2\n".getBytes(StandardCharsets.UTF_8); // 505ns -> 542ns (6.83% slower)

        TestProcess p = new TestProcess(stdout, stderr, true, 2);
        injectProcess(instance, p);

        // First call: process is alive, but there is available stderr. Should return null and accumulate stderr.
        instance.getLivenessCheck();
 // 544ns -> 533ns (2.06% faster)
        // Now simulate the process shutting down
        p.setAlive(false);

        // Second call: should detect process not alive and return an IllegalStateException with both stdout and stderr in the message.
        RuntimeException ex = instance.getLivenessCheck();
 // 15.5μs -> 13.6μs (13.3% faster)
        String msg = ex.getMessage();
        // Should include stdout lines formatted
        // Should include stderr that was previously read and accumulated // 14.0μs -> 12.7μs (10.4% faster)
    }

    @Test
    void testNotAliveNoOutput_returnsExceptionWithExitCodeOnly() throws Exception {
        // Empty stdout and stderr, process already not alive
        TestProcess p = new TestProcess(new byte[0], new byte[0], false, 0);
        injectProcess(instance, p);

        RuntimeException ex = instance.getLivenessCheck();
        // Message should be only the exit code text (trimmed) // 14.0μs -> 12.3μs (14.0% faster)
        String expected = "RPC process shut down early with exit code 0";
    }

    @Test
    void testLargeOutput_handlesLargeContent() throws Exception {
        // Create large stdout and stderr content (~100 KB each) // 15.0μs -> 13.0μs (15.2% faster)
        StringBuilder sbOut = new StringBuilder();
        StringBuilder sbErr = new StringBuilder();
        for (int i = 0; i < 10000; i++) {
            sbOut.append("o");
            sbErr.append("e");
        }
        byte[] stdout = sbOut.toString().getBytes(StandardCharsets.UTF_8);
        byte[] stderr = sbErr.toString().getBytes(StandardCharsets.UTF_8);

        TestProcess p = new TestProcess(stdout, stderr, false, 7);
        injectProcess(instance, p);

        RuntimeException ex = instance.getLivenessCheck();
        String msg = ex.getMessage(); // 31.0μs -> 26.1μs (18.8% faster)
        // Ensure the message includes parts of both large outputs
    }

    @Test
    void testAvailableThrowsIOException_ignoredAndStillProducesException() throws Exception {
        // Create a Process where errorStream.available() throws IOException.
        Process p = new Process() {
            private final ByteArrayInputStream stdout = new ByteArrayInputStream("out".getBytes(StandardCharsets.UTF_8));

            @Override
            public OutputStream getOutputStream() {
                return new ByteArrayOutputStream();
            }

            @Override // 7.36μs -> 7.36μs (0.027% faster)
            public InputStream getInputStream() {
                return stdout;
            }

            @Override
            public InputStream getErrorStream() {
                // InputStream whose available() throws IOException
                return new InputStream() {
                    @Override
                    public int read() {
                        return -1;
                    }

                    @Override
                    public int available() throws IOException {
                        throw new IOException("simulated IO error on available");
                    }
                };
            }

            @Override
            public int waitFor() {
                return 3; // 501μs -> 345μs (45.3% faster)
            }

            @Override
            public int exitValue() {
                return 3;
            }

            @Override
            public void destroy() { }

            @Override
            public Process destroyForcibly() {
                destroy();
                return this;
            }

            @Override
            public boolean isAlive() {
                return false; // process already terminated
            }

            @Override
            public long pid() {
                return 0;
            }

            @Override
            public CompletableFuture<Process> onExit() {
                return CompletableFuture.completedFuture(this);
            }
        };

        injectProcess(instance, p);

        // Even though available() throws IOException, the method should ignore it and continue to produce an exception about the exit.
        RuntimeException ex = instance.getLivenessCheck();
        // stdout was "out" and should be included // 22.8μs -> 21.0μs (8.61% faster)
    }
}

To edit these changes git checkout codeflash/optimize-RewriteRpcProcess.getLivenessCheck-mmn0lc88 and push.

Codeflash Static Badge

The optimization replaced per-call `new byte[available]` allocations with a thread-local reusable 8 KB buffer, eliminating the allocation overhead that line profiler showed consuming ~41 µs (0.9%) in the original. String concatenation for error messages switched from repeated `+=` operations—which create intermediate String objects—to a single `StringBuilder`, cutting the two costliest lines (51% and 30.7% of original runtime) down dramatically. The process field is now cached in a local variable at method entry, avoiding repeated volatile or synchronized field reads. Runtime improved 37% with no correctness trade-offs; all tests pass and the reusable buffer adapts to larger reads when needed.
@codeflash-ai codeflash-ai bot requested a review from HeshamHM28 March 12, 2026 05:15
@codeflash-ai codeflash-ai bot added ⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: High Optimization Quality according to Codeflash labels Mar 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: High Optimization Quality according to Codeflash

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant