feat(core): add HTTP/2 performance gates

This commit is contained in:
Zakaria El Orche
2026-08-13 21:29:21 +00:00
parent 6386264a1e
commit 3679eed74a
20 changed files with 862 additions and 21 deletions
@@ -0,0 +1,141 @@
package dev.relism.flash.http2;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import dev.relism.flash.extension.FlashApp;
import dev.relism.flash.extension.FlashConfiguration;
import java.net.ServerSocket;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.junit.jupiter.api.io.TempDir;
@Tag("benchmark")
@EnabledIfSystemProperty(named = "h2load.executable", matches = ".+")
@EnabledIfSystemProperty(named = "nghttpd.executable", matches = ".+")
class H2LoadMeasurementTest {
private static final int[] CONNECTIONS = {1, 10, 100, 1_000};
private static final int[] STREAMS = {1, 10, 100};
private static final Pattern RATE = Pattern.compile("([0-9.]+) req/s");
private static final Pattern REQUESTS =
Pattern.compile("requests: (\\d+) total, .*? (\\d+) succeeded, (\\d+) failed");
private FlashApp app;
private Process reference;
@AfterEach
void stop() {
if (app != null) app.stop().join();
if (reference != null) reference.destroyForcibly();
}
@Test
void measureFlashAndNghttpdMatrix(@TempDir Path directory) throws Exception {
int flashPort = freePort();
app =
FlashApp.create(
FlashConfiguration.builder()
.host("127.0.0.1")
.port(flashPort)
.http2CleartextEnabled(true)
.h2MaxStreamsCreatedPerInterval(Integer.MAX_VALUE)
.h2MaxStreamsPerConnection(0)
.build());
app.get("/index.html", (request, response) -> "flash-load");
app.start();
int referencePort = freePort();
Files.writeString(directory.resolve("index.html"), "flash-load");
ProcessBuilder server =
new ProcessBuilder(
System.getProperty("nghttpd.executable"),
"--no-tls",
"--max-concurrent-streams=128",
"-d",
directory.toString(),
Integer.toString(referencePort));
applyLibraryPath(server);
reference = server.redirectErrorStream(true).start();
Thread.sleep(200);
System.out.println(
"implementation,connections,requested_streams,effective_streams,requests,requests_per_second");
for (int connections : CONNECTIONS) {
for (int streams : STREAMS) {
int requests = Math.max(1_000, connections * streams);
int effectiveStreams =
Math.max(
1,
Math.min(
Math.min(streams, Http2Limits.MAX_CONCURRENT_STREAMS),
4_096 / connections));
measure("flash", flashPort, connections, streams, effectiveStreams, requests);
measure("nghttpd", referencePort, connections, streams, effectiveStreams, requests);
}
}
}
private static void measure(
String implementation,
int port,
int connections,
int requestedStreams,
int effectiveStreams,
int requests)
throws Exception {
List<String> command = new ArrayList<>();
command.add(System.getProperty("h2load.executable"));
command.add("-n");
command.add(Integer.toString(requests));
command.add("-c");
command.add(Integer.toString(connections));
command.add("-m");
command.add(Integer.toString(effectiveStreams));
command.add("-t");
command.add(Integer.toString(Math.min(8, connections)));
command.add("http://127.0.0.1:" + port + "/index.html");
ProcessBuilder builder = new ProcessBuilder(command).redirectErrorStream(true);
applyLibraryPath(builder);
Process process = builder.start();
assertTrue(process.waitFor(Duration.ofMinutes(2).toMillis(), TimeUnit.MILLISECONDS));
String output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
assertEquals(0, process.exitValue(), output);
Matcher requestsResult = REQUESTS.matcher(output);
assertTrue(requestsResult.find(), output);
assertEquals(requests, Integer.parseInt(requestsResult.group(1)), output);
assertEquals(requests, Integer.parseInt(requestsResult.group(2)), output);
assertEquals(0, Integer.parseInt(requestsResult.group(3)), output);
Matcher rate = RATE.matcher(output);
assertTrue(rate.find(), output);
System.out.printf(
"%s,%d,%d,%d,%d,%s%n",
implementation,
connections,
requestedStreams,
effectiveStreams,
requests,
rate.group(1));
}
private static void applyLibraryPath(ProcessBuilder builder) {
String path = System.getProperty("nghttp.library.path");
if (path != null) builder.environment().put("LD_LIBRARY_PATH", path);
}
private static int freePort() throws Exception {
try (ServerSocket socket = new ServerSocket(0)) {
return socket.getLocalPort();
}
}
}
@@ -2,6 +2,8 @@ package dev.relism.flash.http2.client;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import dev.relism.flash.extension.FlashApp;
import dev.relism.flash.extension.FlashConfiguration;
@@ -10,6 +12,7 @@ import dev.relism.flash.models.MutableHeaderMap;
import dev.relism.flash.tls.TestKeystores;
import dev.relism.flash.tls.TlsConfig;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
@@ -101,6 +104,15 @@ class Http2ClientTest {
}
}
@Test
void configuresConnectionsForRequestResponseLatency() throws Exception {
try (Socket socket = new Socket()) {
assertFalse(socket.getTcpNoDelay());
Http2Client.configureLowLatency(socket);
assertTrue(socket.getTcpNoDelay());
}
}
private static MutableHeaderMap fields(String name, String value) {
MutableHeaderMap headers = new MutableHeaderMap();
byte[] nameBytes = name.getBytes(StandardCharsets.US_ASCII);
@@ -3,6 +3,8 @@ package dev.relism.flash.http2.stream;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -52,4 +54,20 @@ class Http2StreamTableTest {
assertEquals(Http2StreamTable.CLOSED_BY_RESET, table.closedKind(3));
assertEquals(Http2StreamTable.CLOSED_UNKNOWN, table.closedKind(5));
}
@Test
void detachedFinalWriteDoesNotConsumeLiveStreamCapacity() {
Http2StreamTable table = new Http2StreamTable(1);
Http2Stream first = table.acquire(1);
assertTrue(table.detach(first, 1));
Http2Stream second = table.acquire(3);
assertNotNull(second);
assertNotSame(first, second);
table.release(first);
assertTrue(table.retire(second, 3));
assertEquals(2, table.createdCount());
assertEquals(2, table.freeCount());
}
}