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
