diff --git a/frameworks/helidon/Dockerfile b/frameworks/helidon/Dockerfile new file mode 100644 index 00000000..a07b084c --- /dev/null +++ b/frameworks/helidon/Dockerfile @@ -0,0 +1,19 @@ +FROM maven:3.9-eclipse-temurin-25 AS build +WORKDIR /app +COPY pom.xml . +RUN mvn dependency:go-offline -q +COPY src ./src +RUN mvn package -DskipTests -q + +FROM eclipse-temurin:25-jre +WORKDIR /app +COPY --from=build /app/target/helidon-httparena.jar app.jar +COPY --from=build /app/target/libs ./libs +EXPOSE 8080 +ENTRYPOINT ["java", \ + "-server", \ + "-XX:+UseZGC", \ + "-XX:+UseNUMA", \ + "-XX:+AlwaysPreTouch", \ + "-cp", "app.jar:libs/*", \ + "com.httparena.Main"] diff --git a/frameworks/helidon/meta.json b/frameworks/helidon/meta.json new file mode 100644 index 00000000..80ea05d6 --- /dev/null +++ b/frameworks/helidon/meta.json @@ -0,0 +1,21 @@ +{ + "display_name": "helidon", + "language": "Java", + "type": "framework", + "engine": "Níma (Virtual Threads)", + "description": "Helidon SE 4.4 on Níma WebServer with Java 21 virtual threads, Jackson for JSON.", + "repo": "https://github.com/helidon-io/helidon", + "enabled": true, + "tests": [ + "baseline", + "pipelined", + "limited-conn", + "json", + "upload", + "compression", + "noisy", + "mixed", + "async-db", + "static" + ] +} diff --git a/frameworks/helidon/pom.xml b/frameworks/helidon/pom.xml new file mode 100644 index 00000000..8eea0154 --- /dev/null +++ b/frameworks/helidon/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + + io.helidon.applications + helidon-se + 4.4.0 + + + com.httparena + helidon-httparena + 1.0.0 + HttpArena Helidon SE + + + com.httparena.Main + 2.18.3 + + + + + io.helidon.webserver + helidon-webserver + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + org.xerial + sqlite-jdbc + 3.47.2.0 + + + com.zaxxer + HikariCP + 6.2.1 + + + org.postgresql + postgresql + 42.7.5 + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/frameworks/helidon/src/main/java/com/httparena/Main.java b/frameworks/helidon/src/main/java/com/httparena/Main.java new file mode 100644 index 00000000..0fa6c629 --- /dev/null +++ b/frameworks/helidon/src/main/java/com/httparena/Main.java @@ -0,0 +1,386 @@ +package com.httparena; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import io.helidon.http.HeaderNames; +import io.helidon.http.HeaderName; +import io.helidon.http.Status; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.InputStream; +import java.net.URI; +import java.nio.file.Files; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.zip.Deflater; +import java.util.zip.GZIPOutputStream; + +public class Main { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final HeaderName SERVER_HEADER = HeaderNames.create("Server"); + private static final HeaderName CONTENT_TYPE = HeaderNames.CONTENT_TYPE; + private static final HeaderName CONTENT_ENCODING = HeaderNames.CONTENT_ENCODING; + private static final HeaderName ACCEPT_ENCODING = HeaderNames.ACCEPT_ENCODING; + + private static final String DB_QUERY = + "SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE price BETWEEN ? AND ? LIMIT 50"; + + private static List> dataset; + private static byte[] largeJsonResponse; + private static boolean dbAvailable = false; + private static final Map staticFiles = new ConcurrentHashMap<>(); + private static final Map staticContentTypes = new ConcurrentHashMap<>(); + private static HikariDataSource pgPool; + private static final ThreadLocal tlConn = new ThreadLocal<>(); + + private static final Map MIME_TYPES = Map.of( + ".css", "text/css", ".js", "application/javascript", ".html", "text/html", + ".woff2", "font/woff2", ".svg", "image/svg+xml", ".webp", "image/webp", ".json", "application/json" + ); + + public static void main(String[] args) throws Exception { + loadData(); + + WebServer server = WebServer.builder() + .port(8080) + .routing(Main::routing) + .build() + .start(); + + System.out.println("Helidon HttpArena server started on port " + server.port()); + } + + private static void routing(HttpRouting.Builder routing) { + routing.get("/pipeline", Main::pipeline) + .get("/baseline11", Main::baselineGet) + .post("/baseline11", Main::baselinePost) + .get("/baseline2", Main::baseline2) + .get("/json", Main::json) + .get("/compression", Main::compression) + .post("/upload", Main::upload) + .get("/db", Main::db) + .get("/async-db", Main::asyncDb) + .get("/static/{filename}", Main::staticFile); + } + + private static void pipeline(ServerRequest req, ServerResponse res) { + res.header(SERVER_HEADER, "helidon"); + res.header(CONTENT_TYPE, "text/plain"); + res.send("ok"); + } + + private static void baselineGet(ServerRequest req, ServerResponse res) { + long sum = sumQueryParams(req); + res.header(SERVER_HEADER, "helidon"); + res.header(CONTENT_TYPE, "text/plain"); + res.send(String.valueOf(sum)); + } + + private static void baselinePost(ServerRequest req, ServerResponse res) { + long sum = sumQueryParams(req); + String body = req.content().as(String.class).trim(); + try { + sum += Long.parseLong(body); + } catch (NumberFormatException ignored) { + } + res.header(SERVER_HEADER, "helidon"); + res.header(CONTENT_TYPE, "text/plain"); + res.send(String.valueOf(sum)); + } + + private static void baseline2(ServerRequest req, ServerResponse res) { + long sum = sumQueryParams(req); + res.header(SERVER_HEADER, "helidon"); + res.header(CONTENT_TYPE, "text/plain"); + res.send(String.valueOf(sum)); + } + + private static void json(ServerRequest req, ServerResponse res) { + if (dataset == null || dataset.isEmpty()) { + res.status(Status.INTERNAL_SERVER_ERROR_500); + res.header(CONTENT_TYPE, "text/plain"); + res.send("Dataset not loaded"); + return; + } + try { + 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); + } + byte[] body = MAPPER.writeValueAsBytes(Map.of("items", items, "count", items.size())); + res.header(SERVER_HEADER, "helidon"); + res.header(CONTENT_TYPE, "application/json"); + res.send(body); + } catch (Exception e) { + res.status(Status.INTERNAL_SERVER_ERROR_500); + res.send("Error"); + } + } + + private static void compression(ServerRequest req, ServerResponse res) { + if (largeJsonResponse == null || largeJsonResponse.length == 0) { + res.status(Status.INTERNAL_SERVER_ERROR_500); + res.header(CONTENT_TYPE, "text/plain"); + res.send("Dataset not loaded"); + return; + } + try { + String acceptEncoding = req.headers().first(ACCEPT_ENCODING).orElse(""); + res.header(SERVER_HEADER, "helidon"); + res.header(CONTENT_TYPE, "application/json"); + if (acceptEncoding.contains("gzip")) { + res.header(CONTENT_ENCODING, "gzip"); + res.send(gzipCompress(largeJsonResponse)); + } else { + res.send(largeJsonResponse); + } + } catch (Exception e) { + res.status(Status.INTERNAL_SERVER_ERROR_500); + res.send("Error"); + } + } + + private static void upload(ServerRequest req, ServerResponse res) { + try { + InputStream is = req.content().inputStream(); + byte[] buf = new byte[65536]; + long total = 0; + int n; + while ((n = is.read(buf)) != -1) { + total += n; + } + res.header(SERVER_HEADER, "helidon"); + res.header(CONTENT_TYPE, "text/plain"); + res.send(String.valueOf(total)); + } catch (Exception e) { + res.status(Status.INTERNAL_SERVER_ERROR_500); + res.send("Error"); + } + } + + private static void db(ServerRequest req, ServerResponse res) { + if (!dbAvailable) { + res.header(CONTENT_TYPE, "application/json"); + res.send("{\"items\":[],\"count\":0}"); + return; + } + try { + double min = parseDouble(req.query().first("min").orElse("10")); + double max = parseDouble(req.query().first("max").orElse("50")); + Connection conn = getDbConnection(); + List> items = new ArrayList<>(); + PreparedStatement stmt = conn.prepareStatement(DB_QUERY); + stmt.setDouble(1, min); + stmt.setDouble(2, max); + ResultSet rs = stmt.executeQuery(); + while (rs.next()) { + Map item = new LinkedHashMap<>(); + item.put("id", rs.getLong("id")); + item.put("name", rs.getString("name")); + item.put("category", rs.getString("category")); + item.put("price", rs.getDouble("price")); + item.put("quantity", rs.getInt("quantity")); + item.put("active", rs.getInt("active") == 1); + item.put("tags", MAPPER.readValue(rs.getString("tags"), new TypeReference>() {})); + item.put("rating", Map.of("score", rs.getDouble("rating_score"), "count", rs.getInt("rating_count"))); + items.add(item); + } + rs.close(); + stmt.close(); + byte[] body = MAPPER.writeValueAsBytes(Map.of("items", items, "count", items.size())); + res.header(SERVER_HEADER, "helidon"); + res.header(CONTENT_TYPE, "application/json"); + res.send(body); + } catch (Exception e) { + res.header(CONTENT_TYPE, "application/json"); + res.send("{\"items\":[],\"count\":0}"); + } + } + + private static void asyncDb(ServerRequest req, ServerResponse res) { + if (pgPool == null) { + res.header(CONTENT_TYPE, "application/json"); + res.send("{\"items\":[],\"count\":0}"); + return; + } + try { + double min = parseDouble(req.query().first("min").orElse("10")); + double max = parseDouble(req.query().first("max").orElse("50")); + try (Connection conn = pgPool.getConnection()) { + PreparedStatement stmt = conn.prepareStatement(DB_QUERY); + stmt.setDouble(1, min); + stmt.setDouble(2, max); + ResultSet rs = stmt.executeQuery(); + List> items = new ArrayList<>(); + while (rs.next()) { + Map item = new LinkedHashMap<>(); + item.put("id", rs.getLong("id")); + item.put("name", rs.getString("name")); + item.put("category", rs.getString("category")); + item.put("price", rs.getDouble("price")); + item.put("quantity", rs.getInt("quantity")); + item.put("active", rs.getBoolean("active")); + item.put("tags", MAPPER.readValue(rs.getString("tags"), new TypeReference>() {})); + item.put("rating", Map.of("score", rs.getDouble("rating_score"), "count", rs.getInt("rating_count"))); + items.add(item); + } + rs.close(); + stmt.close(); + byte[] body = MAPPER.writeValueAsBytes(Map.of("items", items, "count", items.size())); + res.header(SERVER_HEADER, "helidon"); + res.header(CONTENT_TYPE, "application/json"); + res.send(body); + } + } catch (Exception e) { + res.header(CONTENT_TYPE, "application/json"); + res.send("{\"items\":[],\"count\":0}"); + } + } + + private static void staticFile(ServerRequest req, ServerResponse res) { + String filename = req.path().pathParameters().get("filename"); + byte[] data = staticFiles.get(filename); + if (data == null) { + res.status(Status.NOT_FOUND_404); + res.send(""); + return; + } + String ct = staticContentTypes.getOrDefault(filename, "application/octet-stream"); + res.header(SERVER_HEADER, "helidon"); + res.header(CONTENT_TYPE, ct); + res.send(data); + } + + // --- Helpers --- + + private static long sumQueryParams(ServerRequest req) { + long sum = 0; + for (String name : req.query().names()) { + try { + sum += Long.parseLong(req.query().first(name).orElse("")); + } catch (NumberFormatException ignored) { + } + } + return sum; + } + + private static double parseDouble(String s) { + try { + return Double.parseDouble(s); + } catch (NumberFormatException e) { + return 10.0; + } + } + + private static Connection getDbConnection() { + Connection conn = tlConn.get(); + if (conn == null) { + try { + Properties props = new Properties(); + props.setProperty("open_mode", "1"); // SQLITE_OPEN_READONLY + conn = DriverManager.getConnection("jdbc:sqlite:/data/benchmark.db", props); + conn.createStatement().execute("PRAGMA mmap_size=268435456"); + tlConn.set(conn); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + return conn; + } + + private static byte[] gzipCompress(byte[] data) throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(data.length / 4); + GZIPOutputStream gzip = new GZIPOutputStream(baos) {{ + def.setLevel(Deflater.BEST_SPEED); + }}; + gzip.write(data); + gzip.close(); + return baos.toByteArray(); + } + + private static void loadData() throws Exception { + // 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<>() {}); + } + + // 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())); + } + + // SQLite database + dbAvailable = new File("/data/benchmark.db").exists(); + + // PostgreSQL connection pool + String dbUrl = System.getenv("DATABASE_URL"); + if (dbUrl != null && !dbUrl.isEmpty()) { + try { + URI uri = new URI(dbUrl.replace("postgres://", "postgresql://")); + String host = uri.getHost(); + int port = uri.getPort() > 0 ? uri.getPort() : 5432; + String database = uri.getPath().substring(1); + String[] userInfo = uri.getUserInfo().split(":"); + HikariConfig config = new HikariConfig(); + config.setDriverClassName("org.postgresql.Driver"); + config.setJdbcUrl("jdbc:postgresql://" + host + ":" + port + "/" + database); + config.setUsername(userInfo[0]); + config.setPassword(userInfo.length > 1 ? userInfo[1] : ""); + config.setMaximumPoolSize(64); + config.setMinimumIdle(16); + pgPool = new HikariDataSource(config); + } catch (Exception e) { + System.err.println("PG pool init failed: " + e); + } + } + + // 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()) { + try { + staticFiles.put(sf.getName(), Files.readAllBytes(sf.toPath())); + int dot = sf.getName().lastIndexOf('.'); + String ext = dot >= 0 ? sf.getName().substring(dot) : ""; + staticContentTypes.put(sf.getName(), MIME_TYPES.getOrDefault(ext, "application/octet-stream")); + } catch (Exception ignored) { + } + } + } + } + } + } +}