On Fri, 24 Apr 2026 22:02:15 GMT, Ashay Rane <[email protected]> wrote:

>> Prior to this patch, every HTTP request created a new 16KB buffer for
>> encoding the header, which is typically only a few hundred bytes long.
>> This increased pressure on the garbage collector when the client created
>> lots of requests.  This patch instead makes the header encoder reuse the
>> buffer that is created during the handling of the first request.
>> 
>> The caveat, however, is that the downstream consumers of the header are
>> asynchronous, so the encoder needs to take special care to ensure that
>> it doesn't modify or invalidate the buffer after it hands the buffer
>> over to the downstream asynchronous pipeline.  To resolve this, this
>> patch snapshots the buffer data into compact copies sized to the actual
>> encoded length.  Doing so makes the buffer immediately available for
>> reuse via `clear()` and `limit()`.
>> 
>> For typical requests, this reduces per-request allocation from 16KB to
>> a few hundred bytes (i.e. the size of the compact copy of the encoded
>> headers), with the 16KB encoding buffer allocated once per connection
>> instead of once per request.
>> 
>> ---------
>> - [x] I confirm that I make this contribution in accordance with the 
>> [OpenJDK Interim AI Policy](https://openjdk.org/legal/ai).
>
> Ashay Rane has updated the pull request with a new target base due to a merge 
> or a rebase. The incremental webrev excludes the unrelated changes brought in 
> by the merge/rebase. The pull request contains two additional commits since 
> the last revision:
> 
>  - Merge branch 'master' of https://github.com/openjdk/jdk into 
> JDK-8383248-reuse-buffer-in-header-encoding
>  - Reuse buffer for encoding headers instead of allocating one per request
>    
>    Prior to this patch, every HTTP request created a new 16KB buffer for
>    encoding the header, which are typically only a few hundred bytes long.
>    This increased pressure on the garbage collector when the client created
>    lots of requests.  This patch instead makes the header encoder reuse the
>    buffer that is created during the handling of the first request.
>    
>    The caveat, however, is that the downstream consumers of the header are
>    asynchronous, so the encoder needs to take special care to ensure that
>    it doesn't modify or invalidate the buffer after it hands the buffer
>    over to the downstream asynchronous pipeline.  To resolve this, this
>    patch snapshots the buffer data into compact copies sized to the actual
>    encoded length.  Doing so makes the buffer immediately available for
>    reuse via `clear()` and `limit()`.
>    
>    For typical requests, this reduces per-request allocation from ~16KB to
>    a few hundred bytes (i.e. the size of the compact copy of the encoded
>    headers), with the 16KB encoding buffer allocated once per connection
>    instead of once per request.

I used the following crude benchmark to get some initial GC metrics 
(measurements follow):


package org.openjdk.bench.java.net.http;

import org.openjdk.jmh.annotations.*;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse.BodyHandlers;
import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
@Warmup(iterations = 5, time = 2)
@Measurement(iterations = 5, time = 2)
@Fork(value = 3, jvmArgsAppend = {"--add-modules", "java.net.http"})
public class Http2HeaderEncodingAlloc {

    private HttpClient client;
    private HttpRequest request;
    private ServerSocket ss;
    private volatile boolean running;
    private Thread serverThread;

    @Setup(Level.Trial)
    public void setup() throws Exception {
        ss = new ServerSocket(0, 50, InetAddress.getLoopbackAddress());
        running = true;
        serverThread = new Thread(() -> {
            while (running) {
                try (Socket s = ss.accept()) { serve(s); }
                catch (Exception e) { /* teardown closes ss */ }
            }
        }, "h2c-server");
        serverThread.setDaemon(true);
        serverThread.start();

        client = HttpClient.newBuilder()
                .version(HttpClient.Version.HTTP_2)
                .proxy(HttpClient.Builder.NO_PROXY)
                .build();
        request = HttpRequest.newBuilder()
                .uri(URI.create("http://localhost:"; + ss.getLocalPort() + "/"))
                .header("X-A", "aaa-pad-value")
                .header("X-B", "bbb-pad-value")
                .header("X-C", "ccc-pad-value")
                .GET().build();

        // Warmup: establish the h2c connection
        if (client.send(request, BodyHandlers.discarding()).version()
                != HttpClient.Version.HTTP_2) {
            throw new IllegalStateException("Expected HTTP/2");
        }
    }

    @TearDown(Level.Trial)
    public void teardown() throws Exception {
        running = false;
        try { client.close(); } catch (Exception ignore) { }
        try { ss.close(); } catch (Exception ignore) { }
        serverThread.join(3_000);
    }

    @Benchmark
    public int sendRequest() throws Exception {
        return client.send(request, BodyHandlers.discarding()).statusCode();
    }

    // -- Minimal h2c server --------------------------------------------------

    private void serve(Socket s) throws IOException {
        var in = new DataInputStream(new 
BufferedInputStream(s.getInputStream()));
        var out = new BufferedOutputStream(s.getOutputStream());

        // Consume HTTP/1.1 upgrade request
        while (!readLine(in).isEmpty()) { }

        // 101 Switching Protocols + empty server SETTINGS
        out.write("HTTP/1.1 101 Switching Protocols\r\nConnection: 
Upgrade\r\nUpgrade: h2c\r\n\r\n"
                .getBytes());
        writeFrame(out, 0x4, 0, 0, new byte[0]);
        out.flush();

        // Client connection preface
        in.readFully(new byte[24]);

        // HTTP/2 frame loop: read and dispatch frames per RFC 9113 §4.1
        boolean repliedStream1 = false;
        while (running) {
            // -- Read the 9-byte frame header --
            int len   = ((in.read() & 0xFF) << 16) | ((in.read() & 0xFF) << 8) 
| (in.read() & 0xFF); // 3 bytes: payload length
            int type  = in.read();              // 1 byte: frame type
            int flags = in.read();              // 1 byte: frame flags
            int sid   = in.readInt() & 0x7FFFFFFF; // 4 bytes: stream id (high 
bit reserved)
            // -- Read the frame payload --
            byte[] payload = new byte[len];
            if (len > 0) in.readFully(payload);

            switch (type) {
                // SETTINGS: ACK the client's settings, then respond to
                // stream 1 (the implicit h2c upgrade request)
                case 0x4 -> {
                    if ((flags & 0x1) == 0) {   // not already an ACK
                        writeFrame(out, 0x4, 0x1, 0, new byte[0]); // SETTINGS 
ACK
                        if (!repliedStream1) { reply(out, 1); repliedStream1 = 
true; }
                        out.flush();
                    }
                }
                // HEADERS: a new request; send back :status 200
                case 0x1 -> { reply(out, sid); out.flush(); }
                // PING: echo the 8-byte payload back with the ACK flag
                case 0x6 -> { writeFrame(out, 0x6, 0x1, 0, payload); 
out.flush(); }
                // GOAWAY: client is closing the connection
                case 0x7 -> { return; }
                // WINDOW_UPDATE, DATA, etc.: safe to ignore for this benchmark
                default  -> { }
            }
        }
    }

    /**
     * Send a HEADERS frame with HPACK-encoded ":status 200".
     * 0x1 = HEADERS frame type, 0x5 = END_STREAM | END_HEADERS flags,
     * 0x88 = HPACK indexed representation of ":status: 200" (static table 
index 8).
     */
    private static void reply(OutputStream out, int sid) throws IOException {
        writeFrame(out, 0x1, 0x5, sid, new byte[]{(byte) 0x88});
    }

    /**
     * Write a raw HTTP/2 frame: 9-byte header (3 bytes length, 1 byte type,
     * 1 byte flags, 4 bytes stream id) followed by the payload.
     */
    private static void writeFrame(OutputStream out, int type, int flags,
            int sid, byte[] p) throws IOException {
        out.write(new byte[]{
                (byte) (p.length >> 16), (byte) (p.length >> 8), (byte) 
p.length, // 3 bytes: payload length
                (byte) type,                                                    
   // 1 byte:  frame type
                (byte) flags,                                                   
   // 1 byte:  flags
                (byte) ((sid >> 24) & 0x7F), (byte) (sid >> 16), (byte) (sid >> 
8), (byte) sid}); // 4 bytes: stream id
        if (p.length > 0) out.write(p);
    }

    private static String readLine(DataInputStream in) throws IOException {
        var sb = new StringBuilder();
        int b;
        while ((b = in.read()) != '\r') sb.append((char) b);
        in.read(); // \n
        return sb.toString();
    }
}


Results:


== old ==
Benchmark                                                Mode  Cnt      Score   
 Error   Units
Http2HeaderEncodingAlloc.sendRequest                     avgt   15    183.682 
┬▒  7.053   us/op
Http2HeaderEncodingAlloc.sendRequest:gc.alloc.rate       avgt   15    152.896 
┬▒  5.959  MB/sec
Http2HeaderEncodingAlloc.sendRequest:gc.alloc.rate.norm  avgt   15  29444.372 
┬▒ 19.060    B/op
Http2HeaderEncodingAlloc.sendRequest:gc.count            avgt   15     67.000   
        counts
Http2HeaderEncodingAlloc.sendRequest:gc.cpuTime          avgt   15     92.000   
            ms
Http2HeaderEncodingAlloc.sendRequest:gc.time             avgt   15     56.000   
            ms

== new ==
Benchmark                                                Mode  Cnt      Score   
 Error   Units
Http2HeaderEncodingAlloc.sendRequest                     avgt   15    191.926 
┬▒  5.987   us/op
Http2HeaderEncodingAlloc.sendRequest:gc.alloc.rate       avgt   15     65.452 
┬▒  2.000  MB/sec
Http2HeaderEncodingAlloc.sendRequest:gc.alloc.rate.norm  avgt   15  13176.388 
┬▒ 43.059    B/op
Http2HeaderEncodingAlloc.sendRequest:gc.count            avgt   15     49.000   
        counts
Http2HeaderEncodingAlloc.sendRequest:gc.cpuTime          avgt   15    107.000   
            ms
Http2HeaderEncodingAlloc.sendRequest:gc.time             avgt   15     64.000   
            ms

-------------

PR Comment: https://git.openjdk.org/jdk/pull/30931#issuecomment-4330459981

Reply via email to