diff --git a/frameworks/vertx/Dockerfile b/frameworks/vertx/Dockerfile new file mode 100644 index 0000000..26bc87a --- /dev/null +++ b/frameworks/vertx/Dockerfile @@ -0,0 +1,26 @@ +FROM maven:3.9-eclipse-temurin-21 AS build +WORKDIR /app +COPY pom.xml . +RUN mvn dependency:go-offline -q +COPY src ./src +RUN mvn package -DskipTests -q + +FROM eclipse-temurin:21-jre +WORKDIR /app +COPY --from=build /app/target/vertx-httparena-1.0.0.jar /app/app.jar +EXPOSE 8080 8443 +ENTRYPOINT ["java", \ + "-server", \ + "-XX:+UseParallelGC", \ + "-XX:+UseNUMA", \ + "-XX:-StackTraceInThrowable", \ + "-Dio.netty.buffer.checkBounds=false", \ + "-Dio.netty.buffer.checkAccessible=false", \ + "-Dvertx.disableURIValidation=true", \ + "-Dvertx.disableHttpHeadersValidation=true", \ + "-Dvertx.disableMetrics=true", \ + "-Dvertx.disableH2c=true", \ + "-Dvertx.disableWebsockets=true", \ + "-Dvertx.threadChecks=false", \ + "-Dvertx.disableContextTimings=true", \ + "-jar", "app.jar"] diff --git a/frameworks/vertx/README.md b/frameworks/vertx/README.md new file mode 100644 index 0000000..15792b5 --- /dev/null +++ b/frameworks/vertx/README.md @@ -0,0 +1,22 @@ +# Vert.x — HttpArena + +[Eclipse Vert.x](https://github.com/eclipse-vertx/vert.x) is a reactive toolkit for building high-performance applications on the JVM. Unlike traditional frameworks (Spring, Quarkus), Vert.x uses an event-loop threading model directly on Netty — no annotation scanning, no dependency injection, no framework overhead. + +## Key Details + +- **Vert.x 4.5.14** (latest stable) with vertx-web for routing +- **Event-loop architecture**: one verticle instance per CPU core, each on its own event loop +- **JDK 21** with ParallelGC +- **Jackson** for JSON serialization +- **SQLite** via JDBC (executed on worker threads via `executeBlocking()` to avoid blocking event loops) +- **Pre-computed** JSON and gzip responses at startup +- **Netty native transport** enabled (`preferNativeTransport`) + +## Architecture Notes + +Vert.x sits in a unique spot in the JVM ecosystem: +- **Spring** = DI + annotations + Tomcat/Netty (high-level framework) +- **Quarkus** = CDI + annotations + Vert.x/Netty under the hood (compile-time optimization) +- **Vert.x** = event loops + handlers + Netty directly (reactive toolkit) + +Quarkus actually uses Vert.x internally, so this benchmark shows the raw Vert.x performance vs. the Quarkus abstraction on top. diff --git a/frameworks/vertx/meta.json b/frameworks/vertx/meta.json new file mode 100644 index 0000000..29761ba --- /dev/null +++ b/frameworks/vertx/meta.json @@ -0,0 +1,21 @@ +{ + "display_name": "vertx", + "language": "Java", + "type": "framework", + "engine": "Netty", + "description": "Eclipse Vert.x 4.5 — reactive toolkit on Netty with event-loop threading, JDK 21.", + "repo": "https://github.com/eclipse-vertx/vert.x", + "enabled": true, + "tests": [ + "baseline", + "pipelined", + "limited-conn", + "json", + "upload", + "compression", + "noisy", + "mixed", + "baseline-h2", + "static-h2" + ] +} diff --git a/frameworks/vertx/pom.xml b/frameworks/vertx/pom.xml new file mode 100644 index 0000000..5c5fba8 --- /dev/null +++ b/frameworks/vertx/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + + com.httparena + vertx-httparena + 1.0.0 + jar + + + 21 + 21 + UTF-8 + 4.5.14 + 2.16.1 + + + + + io.vertx + vertx-core + ${vertx.version} + + + io.vertx + vertx-web + ${vertx.version} + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + org.xerial + sqlite-jdbc + 3.47.2.0 + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + + package + + shade + + + + + com.httparena.MainVerticle + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + diff --git a/frameworks/vertx/src/main/java/com/httparena/MainVerticle.java b/frameworks/vertx/src/main/java/com/httparena/MainVerticle.java new file mode 100644 index 0000000..f203e9f --- /dev/null +++ b/frameworks/vertx/src/main/java/com/httparena/MainVerticle.java @@ -0,0 +1,339 @@ +package com.httparena; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.vertx.core.*; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.*; +import io.vertx.core.net.PemKeyCertOptions; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.BodyHandler; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.sql.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class MainVerticle extends AbstractVerticle { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final Buffer OK_BUFFER = Buffer.buffer("ok"); + + // Shared pre-computed data (loaded once, shared across verticle instances) + private static volatile List> dataset; + private static volatile byte[] jsonResponse; + private static volatile byte[] largeJsonResponse; + private static volatile byte[] gzipLargeJsonResponse; + private static final Map staticFiles = new ConcurrentHashMap<>(); + private static final Map MIME_TYPES = Map.ofEntries( + Map.entry(".css", "text/css"), + Map.entry(".js", "application/javascript"), + Map.entry(".html", "text/html"), + Map.entry(".woff2", "font/woff2"), + Map.entry(".svg", "image/svg+xml"), + Map.entry(".webp", "image/webp"), + Map.entry(".json", "application/json") + ); + + private static final Object INIT_LOCK = new Object(); + private static volatile boolean dataInitialized = false; + + // Per-verticle DB connection (one per event loop — no contention) + private Connection dbConn; + private PreparedStatement dbStmt; + + public static void main(String[] args) { + int instances = Runtime.getRuntime().availableProcessors(); + VertxOptions vertxOpts = new VertxOptions() + .setPreferNativeTransport(true) + .setEventLoopPoolSize(instances); + + Vertx vertx = Vertx.vertx(vertxOpts); + + // Pre-load shared data before deploying verticles + initSharedData(); + + DeploymentOptions deployOpts = new DeploymentOptions().setInstances(instances); + vertx.deployVerticle(MainVerticle.class.getName(), deployOpts) + .onSuccess(id -> System.out.println("Deployed " + instances + " instances")) + .onFailure(err -> { + err.printStackTrace(); + System.exit(1); + }); + } + + private static void initSharedData() { + if (dataInitialized) return; + synchronized (INIT_LOCK) { + if (dataInitialized) return; + try { + // Dataset + String path = System.getenv("DATASET_PATH"); + if (path == null) path = "/data/dataset.json"; + File f = new File(path); + if (f.exists()) { + dataset = MAPPER.readValue(f, new TypeReference<>() {}); + // Pre-compute /json response + List> items = new ArrayList<>(dataset.size()); + for (Map item : dataset) { + Map processed = new LinkedHashMap<>(item); + double price = ((Number) item.get("price")).doubleValue(); + int quantity = ((Number) item.get("quantity")).intValue(); + processed.put("total", Math.round(price * quantity * 100.0) / 100.0); + items.add(processed); + } + jsonResponse = MAPPER.writeValueAsBytes(Map.of("items", items, "count", items.size())); + } + + // Large dataset for compression + File largef = new File("/data/dataset-large.json"); + if (largef.exists()) { + List> largeDataset = MAPPER.readValue(largef, new TypeReference<>() {}); + List> largeItems = new ArrayList<>(largeDataset.size()); + for (Map item : largeDataset) { + Map processed = new LinkedHashMap<>(item); + double price = ((Number) item.get("price")).doubleValue(); + int quantity = ((Number) item.get("quantity")).intValue(); + processed.put("total", Math.round(price * quantity * 100.0) / 100.0); + largeItems.add(processed); + } + largeJsonResponse = MAPPER.writeValueAsBytes(Map.of("items", largeItems, "count", largeItems.size())); + + // Pre-gzip + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + java.util.zip.GZIPOutputStream gz = new java.util.zip.GZIPOutputStream(baos); + gz.write(largeJsonResponse); + gz.close(); + gzipLargeJsonResponse = baos.toByteArray(); + } + + // Static files + File staticDir = new File("/data/static"); + if (staticDir.isDirectory()) { + File[] files = staticDir.listFiles(); + if (files != null) { + for (File sf : files) { + if (sf.isFile()) { + staticFiles.put(sf.getName(), Files.readAllBytes(sf.toPath())); + } + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + dataInitialized = true; + } + } + + @Override + public void start(Promise startPromise) { + // Per-verticle DB connection + File dbFile = new File("/data/benchmark.db"); + if (dbFile.exists()) { + try { + dbConn = DriverManager.getConnection("jdbc:sqlite:file:/data/benchmark.db?mode=ro&immutable=1"); + dbConn.createStatement().execute("PRAGMA mmap_size=268435456"); + dbStmt = dbConn.prepareStatement( + "SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE price BETWEEN ? AND ? LIMIT 50"); + } catch (Exception e) { + e.printStackTrace(); + } + } + + Router router = Router.router(vertx); + + // Body handler for POST requests — 25MB limit for upload + router.post().handler(BodyHandler.create().setBodyLimit(25 * 1024 * 1024)); + + // Routes + router.get("/pipeline").handler(this::handlePipeline); + router.get("/baseline11").handler(this::handleBaselineGet); + router.post("/baseline11").handler(this::handleBaselinePost); + router.get("/baseline2").handler(this::handleBaseline2); + router.get("/json").handler(this::handleJson); + router.get("/compression").handler(this::handleCompression); + router.post("/upload").handler(this::handleUpload); + router.get("/db").handler(this::handleDb); + router.get("/static/:filename").handler(this::handleStatic); + + // Catch-all: return 404 for unmatched routes + router.route().handler(ctx -> ctx.response().setStatusCode(404).end()); + + // HTTP/1.1 on port 8080 + HttpServerOptions httpOpts = new HttpServerOptions() + .setPort(8080) + .setHost("0.0.0.0") + .setTcpNoDelay(true) + .setTcpFastOpen(true) + .setCompressionSupported(false) // We handle compression manually for /compression + .setIdleTimeout(0); + + vertx.createHttpServer(httpOpts) + .requestHandler(router) + .listen() + .compose(http -> { + // HTTP/2 + TLS on port 8443 (if certs exist) + File cert = new File("/certs/server.crt"); + File key = new File("/certs/server.key"); + if (cert.exists() && key.exists()) { + HttpServerOptions httpsOpts = new HttpServerOptions() + .setPort(8443) + .setHost("0.0.0.0") + .setSsl(true) + .setUseAlpn(true) + .setKeyCertOptions(new PemKeyCertOptions() + .setCertPath("/certs/server.crt") + .setKeyPath("/certs/server.key")) + .setTcpNoDelay(true) + .setTcpFastOpen(true) + .setCompressionSupported(false) + .setIdleTimeout(0); + + return vertx.createHttpServer(httpsOpts) + .requestHandler(router) + .listen(); + } + return Future.succeededFuture(); + }) + .onSuccess(v -> startPromise.complete()) + .onFailure(startPromise::fail); + } + + private void handlePipeline(RoutingContext ctx) { + ctx.response() + .putHeader("content-type", "text/plain") + .end(OK_BUFFER); + } + + private void handleBaselineGet(RoutingContext ctx) { + int sum = sumParams(ctx); + ctx.response() + .putHeader("content-type", "text/plain") + .end(String.valueOf(sum)); + } + + private void handleBaselinePost(RoutingContext ctx) { + int sum = sumParams(ctx); + String body = ctx.body().asString(); + if (body != null && !body.isEmpty()) { + try { + sum += Integer.parseInt(body.trim()); + } catch (NumberFormatException ignored) {} + } + ctx.response() + .putHeader("content-type", "text/plain") + .end(String.valueOf(sum)); + } + + private void handleBaseline2(RoutingContext ctx) { + int sum = sumParams(ctx); + ctx.response() + .putHeader("content-type", "text/plain") + .end(String.valueOf(sum)); + } + + private void handleJson(RoutingContext ctx) { + if (jsonResponse == null) { + ctx.response().setStatusCode(500).end("Dataset not loaded"); + return; + } + ctx.response() + .putHeader("content-type", "application/json") + .end(Buffer.buffer(jsonResponse)); + } + + private void handleCompression(RoutingContext ctx) { + if (largeJsonResponse == null) { + ctx.response().setStatusCode(500).end("Large dataset not loaded"); + return; + } + String acceptEncoding = ctx.request().getHeader("Accept-Encoding"); + if (acceptEncoding != null && acceptEncoding.contains("gzip") && gzipLargeJsonResponse != null) { + ctx.response() + .putHeader("content-type", "application/json") + .putHeader("content-encoding", "gzip") + .end(Buffer.buffer(gzipLargeJsonResponse)); + } else { + ctx.response() + .putHeader("content-type", "application/json") + .end(Buffer.buffer(largeJsonResponse)); + } + } + + private void handleUpload(RoutingContext ctx) { + Buffer body = ctx.body().buffer(); + int len = body != null ? body.length() : 0; + ctx.response() + .putHeader("content-type", "text/plain") + .end(String.valueOf(len)); + } + + private void handleDb(RoutingContext ctx) { + if (dbStmt == null) { + ctx.response().setStatusCode(500).end("DB not available"); + return; + } + // DB is blocking — execute on worker thread + vertx.executeBlocking(() -> { + double minPrice = 10.0, maxPrice = 50.0; + String minParam = ctx.request().getParam("min"); + String maxParam = ctx.request().getParam("max"); + if (minParam != null) try { minPrice = Double.parseDouble(minParam); } catch (NumberFormatException ignored) {} + if (maxParam != null) try { maxPrice = Double.parseDouble(maxParam); } catch (NumberFormatException ignored) {} + + dbStmt.setDouble(1, minPrice); + dbStmt.setDouble(2, maxPrice); + ResultSet rs = dbStmt.executeQuery(); + List> items = new ArrayList<>(); + while (rs.next()) { + List tags = MAPPER.readValue(rs.getString(7), new TypeReference<>() {}); + Map row = new LinkedHashMap<>(); + row.put("id", rs.getInt(1)); + row.put("name", rs.getString(2)); + row.put("category", rs.getString(3)); + row.put("price", rs.getDouble(4)); + row.put("quantity", rs.getInt(5)); + row.put("active", rs.getInt(6) == 1); + row.put("tags", tags); + row.put("rating", Map.of("score", rs.getDouble(8), "count", rs.getInt(9))); + items.add(row); + } + rs.close(); + return MAPPER.writeValueAsBytes(Map.of("items", items, "count", items.size())); + }).onSuccess(bytes -> { + ctx.response() + .putHeader("content-type", "application/json") + .end(Buffer.buffer(bytes)); + }).onFailure(err -> { + ctx.response().setStatusCode(500).end(err.getMessage()); + }); + } + + private void handleStatic(RoutingContext ctx) { + String filename = ctx.pathParam("filename"); + byte[] data = staticFiles.get(filename); + if (data == null) { + ctx.response().setStatusCode(404).end(); + return; + } + int dot = filename.lastIndexOf('.'); + String ext = dot >= 0 ? filename.substring(dot) : ""; + String ct = MIME_TYPES.getOrDefault(ext, "application/octet-stream"); + ctx.response() + .putHeader("content-type", ct) + .end(Buffer.buffer(data)); + } + + private int sumParams(RoutingContext ctx) { + int sum = 0; + String a = ctx.request().getParam("a"); + String b = ctx.request().getParam("b"); + if (a != null) try { sum += Integer.parseInt(a); } catch (NumberFormatException ignored) {} + if (b != null) try { sum += Integer.parseInt(b); } catch (NumberFormatException ignored) {} + return sum; + } +}