diff --git a/frameworks/Java/netty-fast/.sdkmanrc b/frameworks/Java/netty-fast/.sdkmanrc new file mode 100644 index 00000000000..b68ac92e765 --- /dev/null +++ b/frameworks/Java/netty-fast/.sdkmanrc @@ -0,0 +1,3 @@ +# Enable auto-env through the sdkman_auto_env config +# Add key=value pairs of SDKs to use below +java=25-oracle diff --git a/frameworks/Java/netty-fast/README.md b/frameworks/Java/netty-fast/README.md new file mode 100644 index 00000000000..d771217fcb7 --- /dev/null +++ b/frameworks/Java/netty-fast/README.md @@ -0,0 +1,60 @@ +# Netty (Fast / Minimal) + +This is a Netty-based implementation for the TechEmpower Framework Benchmarks. +It is a minimal, high-performance HTTP/1.1 server built directly on Netty +primitives with a focus on correctness and low overhead. + +The implementation uses real HTTP parsing, proper response headers, and +standards-compliant behavior while aggressively minimizing allocations and +framework abstractions. + +## Tests + +### Plaintext Test +Responds with a static plaintext message. + +GET /plaintext + +Response body: +Hello, World! + +Content-Type: +text/plain + +### JSON Test +Responds with a JSON-encoded object using a real JSON serializer. + +GET /json + +Response body: +{"message":"Hello, World!"} + +Content-Type: +application/json + +JSON serialization is performed using fastjson2. + +## Implementation Notes + +- HTTP/1.1 only +- Pipelining supported +- Uses Netty event loops and pooled buffers +- Minimal channel pipeline +- No framework abstractions beyond Netty itself +- Optimized for low allocation rate and high throughput +- Designed to match TechEmpower benchmark rules and expectations + +This implementation is intended to demonstrate the maximum achievable +performance of Netty when used as a low-level HTTP server. + +## Versions + +- Java 24+ (tested with Java 24 / 25) +- Netty 4.2.x +- fastjson2 + +## References + +- https://netty.io/ +- https://github.com/netty/netty +- https://github.com/TechEmpower/FrameworkBenchmarks diff --git a/frameworks/Java/netty-fast/benchmark_config.json b/frameworks/Java/netty-fast/benchmark_config.json new file mode 100644 index 00000000000..14dd7c21495 --- /dev/null +++ b/frameworks/Java/netty-fast/benchmark_config.json @@ -0,0 +1,24 @@ +{ + "framework": "netty-fast", + "tests": [{ + "default": { + "json_url": "/json", + "plaintext_url": "/plaintext", + "port": 8080, + "approach": "Realistic", + "classification": "Platform", + "database": "None", + "framework": "netty-fast", + "language": "Java", + "flavor": "None", + "orm": "Raw", + "platform": "Netty", + "webserver": "None", + "os": "Linux", + "database_os": "Linux", + "display_name": "netty-fast", + "notes": "", + "versus": "netty-fast" + } + }] +} diff --git a/frameworks/Java/netty-fast/config.toml b/frameworks/Java/netty-fast/config.toml new file mode 100644 index 00000000000..a7ff0829c33 --- /dev/null +++ b/frameworks/Java/netty-fast/config.toml @@ -0,0 +1,15 @@ +[framework] +name = "netty-fast" + +[main] +urls.plaintext = "/plaintext" +urls.json = "/json" +approach = "Realistic" +classification = "Platform" +database = "None" +database_os = "Linux" +os = "Linux" +orm = "Raw" +platform = "Netty" +webserver = "None" +versus = "netty-fast" diff --git a/frameworks/Java/netty-fast/netty-fast.dockerfile b/frameworks/Java/netty-fast/netty-fast.dockerfile new file mode 100644 index 00000000000..1b7545d40da --- /dev/null +++ b/frameworks/Java/netty-fast/netty-fast.dockerfile @@ -0,0 +1,13 @@ +FROM maven:3.9-eclipse-temurin-25-noble as maven +WORKDIR /netty-fast +COPY pom.xml pom.xml +COPY src src +RUN mvn -q -DskipTests package + +FROM eclipse-temurin:25-jre-noble +WORKDIR /netty-fast +COPY --from=maven /netty-fast/target/app.jar ./app.jar +COPY run_netty.sh ./run_netty.sh +RUN chmod +x ./run_netty.sh +EXPOSE 8080 +ENTRYPOINT ["./run_netty.sh"] diff --git a/frameworks/Java/netty-fast/pom.xml b/frameworks/Java/netty-fast/pom.xml new file mode 100644 index 00000000000..a1c2cad444a --- /dev/null +++ b/frameworks/Java/netty-fast/pom.xml @@ -0,0 +1,101 @@ + + + 4.0.0 + + com.techempower + netty-example + 0.1 + + + 24 + 24 + 4.2.0.Final + + + jar + + + + + io.netty + netty-all + ${netty.version} + + + + io.netty + netty-transport-native-epoll + ${netty.version} + linux-x86_64 + + + + io.netty + netty-transport-native-kqueue + ${netty.version} + osx-x86_64 + + + + io.netty + netty-transport-native-kqueue + ${netty.version} + osx-aarch_64 + + + + io.netty + netty-transport-native-io_uring + ${netty.version} + linux-x86_64 + + + + com.alibaba.fastjson2 + fastjson2 + 2.0.53 + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + false + + + + + maven-assembly-plugin + + app + + + hello.HelloWebServer + + + + jar-with-dependencies + + false + + + + make-assembly + package + + single + + + + + + + + diff --git a/frameworks/Java/netty-fast/run_netty.sh b/frameworks/Java/netty-fast/run_netty.sh new file mode 100755 index 00000000000..41972b17d50 --- /dev/null +++ b/frameworks/Java/netty-fast/run_netty.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# PROFILING: -XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints +JAVA_OPTIONS="--enable-native-access=ALL-UNNAMED \ + -Dio.netty.noUnsafe=false \ + --sun-misc-unsafe-memory-access=allow \ + --add-opens=java.base/java.lang=ALL-UNNAMED \ + -XX:+UseNUMA \ + -XX:+UseParallelGC \ + -Dio.netty.buffer.checkBounds=false \ + -Dio.netty.buffer.checkAccessible=false \ + $@" + +exec java $JAVA_OPTIONS -jar app.jar \ No newline at end of file diff --git a/frameworks/Java/netty-fast/src/main/java/hello/Constants.java b/frameworks/Java/netty-fast/src/main/java/hello/Constants.java new file mode 100644 index 00000000000..1b1438cbf11 --- /dev/null +++ b/frameworks/Java/netty-fast/src/main/java/hello/Constants.java @@ -0,0 +1,19 @@ +package hello; + +import io.netty.util.AsciiString; +import io.netty.util.CharsetUtil; + +public final class Constants { + + public static final byte[] STATIC_PLAINTEXT = "Hello, World!".getBytes(CharsetUtil.UTF_8); + public static final int STATIC_PLAINTEXT_LEN = STATIC_PLAINTEXT.length; + public static final CharSequence PLAINTEXT_CLHEADER_VALUE = + AsciiString.cached(String.valueOf(STATIC_PLAINTEXT_LEN)); + + public static final CharSequence SERVER_NAME = AsciiString.cached("Netty"); + + public static final Message STATIC_MESSAGE = new Message("Hello, World!"); + + private Constants() { + } +} diff --git a/frameworks/Java/netty-fast/src/main/java/hello/HelloServerHandler.java b/frameworks/Java/netty-fast/src/main/java/hello/HelloServerHandler.java new file mode 100644 index 00000000000..c356bd2fa55 --- /dev/null +++ b/frameworks/Java/netty-fast/src/main/java/hello/HelloServerHandler.java @@ -0,0 +1,256 @@ +package hello; + +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.util.AttributeKey; +import io.netty.util.concurrent.FastThreadLocal; + +public class HelloServerHandler extends ChannelInboundHandlerAdapter { + + private static final AttributeKey STATE_KEY = + AttributeKey.valueOf("hello.state"); + + private static final FastThreadLocal CACHE_TL = new FastThreadLocal<>(); + + private static final byte[] NOT_FOUND_BYTES = + "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n" + .getBytes(StandardCharsets.US_ASCII); + + private static final byte[] JSON_BODY = JsonUtils.serializeMsg(Constants.STATIC_MESSAGE); + + private final ResponseCache cache; + + public HelloServerHandler(ScheduledExecutorService scheduledExecutor) { + ResponseCache c = CACHE_TL.get(); + if (c == null) { + c = new ResponseCache(scheduledExecutor); + CACHE_TL.set(c); + } + this.cache = c; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + ctx.channel().attr(STATE_KEY).set(new PerChannelState()); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + PerChannelState state = ctx.channel().attr(STATE_KEY).get(); + if (state != null && state.outAggregate != null) { + state.outAggregate.release(); + state.outAggregate = null; + state.hadBatched = false; + } + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + try { + if (msg instanceof HttpRequest req) { + handleRequest(ctx, state(ctx), req); + return; + } + + if (msg instanceof HttpContent content) { + content.release(); + if (msg instanceof LastHttpContent) { + return; + } + return; + } + + io.netty.util.ReferenceCountUtil.release(msg); + } catch (Throwable t) { + PerChannelState st = ctx.channel().attr(STATE_KEY).get(); + if (st != null && st.outAggregate != null) { + st.outAggregate.release(); + st.outAggregate = null; + st.hadBatched = false; + } + ctx.close(); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + PerChannelState state = state(ctx); + if (state.hadBatched && state.outAggregate != null) { + ByteBuf out = state.outAggregate; + state.outAggregate = null; + state.hadBatched = false; + ctx.writeAndFlush(out).addListener(ChannelFutureListener.CLOSE_ON_FAILURE); + } else { + ctx.flush(); + } + } + + private void handleRequest(ChannelHandlerContext ctx, PerChannelState state, HttpRequest req) { + state.closeAfterFlush |= !HttpUtil.isKeepAlive(req); + + final String uri = req.uri(); + if ("/plaintext".equals(uri)) { + encodePlaintext(ctx, state); + return; + } + if ("/json".equals(uri)) { + encodeJson(ctx, state); + return; + } + + writeNotFound(ctx, state); + } + + private PerChannelState state(ChannelHandlerContext ctx) { + PerChannelState st = ctx.channel().attr(STATE_KEY).get(); + if (st == null) { + st = new PerChannelState(); + ctx.channel().attr(STATE_KEY).set(st); + } + return st; + } + + private ByteBuf ensureAggregate(ChannelHandlerContext ctx, PerChannelState state) { + if (state.outAggregate == null) { + state.outAggregate = ctx.alloc().buffer(256); + } + return state.outAggregate; + } + + private void encodePlaintext(ChannelHandlerContext ctx, PerChannelState state) { + ByteBuf agg = ensureAggregate(ctx, state); + agg.writeBytes(cache.plaintextResponseBytes()); + state.hadBatched = true; + } + + private void encodeJson(ChannelHandlerContext ctx, PerChannelState state) { + ByteBuf agg = ensureAggregate(ctx, state); + agg.writeBytes(cache.jsonResponseBytes()); + state.hadBatched = true; + } + + private void writeNotFound(ChannelHandlerContext ctx, PerChannelState state) { + if (state.outAggregate != null) { + ByteBuf out = state.outAggregate; + state.outAggregate = null; + state.hadBatched = false; + + if (state.closeAfterFlush) { + ctx.write(out); + ctx.write(Unpooled.wrappedBuffer(NOT_FOUND_BYTES)); + ctx.flush(); + ctx.close(); + state.closeAfterFlush = false; + return; + } + + ctx.writeAndFlush(out).addListener(ChannelFutureListener.CLOSE_ON_FAILURE); + } + + if (state.closeAfterFlush) { + ctx.writeAndFlush(Unpooled.wrappedBuffer(NOT_FOUND_BYTES)) + .addListener(ChannelFutureListener.CLOSE_ON_FAILURE) + .addListener(ChannelFutureListener.CLOSE); + state.closeAfterFlush = false; + } else { + ctx.writeAndFlush(Unpooled.wrappedBuffer(NOT_FOUND_BYTES)) + .addListener(ChannelFutureListener.CLOSE_ON_FAILURE); + } + } + + private static final class PerChannelState { + ByteBuf outAggregate; + boolean hadBatched; + boolean closeAfterFlush; + } + + + private static final class ResponseCache implements Runnable { + private static final DateTimeFormatter RFC_1123 = + DateTimeFormatter.RFC_1123_DATE_TIME.withZone(ZoneOffset.UTC); + + private static final byte[] PLAIN_PREFIX = + ("HTTP/1.1 200 OK\r\n" + + "Server: Netty\r\n" + + "Date: ").getBytes(StandardCharsets.US_ASCII); + + private static final byte[] PLAIN_MID = + ("\r\n" + + "Content-Type: text/plain\r\n" + + "Content-Length: " + Constants.STATIC_PLAINTEXT_LEN + "\r\n" + + "\r\n").getBytes(StandardCharsets.US_ASCII); + + private static final byte[] JSON_PREFIX = + ("HTTP/1.1 200 OK\r\n" + + "Server: Netty\r\n" + + "Date: ").getBytes(StandardCharsets.US_ASCII); + + private static final byte[] JSON_MID = + ("\r\n" + + "Content-Type: application/json\r\n" + + "Content-Length: " + JSON_BODY.length + "\r\n" + + "\r\n").getBytes(StandardCharsets.US_ASCII); + + private final ScheduledExecutorService scheduler; + + private volatile byte[] plaintextResponse; + private volatile byte[] jsonResponse; + + ResponseCache(ScheduledExecutorService scheduler) { + this.scheduler = scheduler; + rebuildNow(); // first responses already have Date + this.scheduler.scheduleAtFixedRate(this, 1, 1, TimeUnit.SECONDS); + } + + byte[] plaintextResponseBytes() { + return plaintextResponse; + } + + byte[] jsonResponseBytes() { + return jsonResponse; + } + + @Override + public void run() { + rebuildNow(); + } + + private void rebuildNow() { + String date = RFC_1123.format(Instant.now()); + byte[] dateBytes = date.getBytes(StandardCharsets.US_ASCII); + + // plaintext: prefix + date + mid + body + byte[] plain = new byte[PLAIN_PREFIX.length + dateBytes.length + PLAIN_MID.length + Constants.STATIC_PLAINTEXT_LEN]; + int p = 0; + System.arraycopy(PLAIN_PREFIX, 0, plain, p, PLAIN_PREFIX.length); p += PLAIN_PREFIX.length; + System.arraycopy(dateBytes, 0, plain, p, dateBytes.length); p += dateBytes.length; + System.arraycopy(PLAIN_MID, 0, plain, p, PLAIN_MID.length); p += PLAIN_MID.length; + System.arraycopy(Constants.STATIC_PLAINTEXT, 0, plain, p, Constants.STATIC_PLAINTEXT_LEN); + + // json: prefix + date + mid + body + byte[] json = new byte[JSON_PREFIX.length + dateBytes.length + JSON_MID.length + JSON_BODY.length]; + int j = 0; + System.arraycopy(JSON_PREFIX, 0, json, j, JSON_PREFIX.length); j += JSON_PREFIX.length; + System.arraycopy(dateBytes, 0, json, j, dateBytes.length); j += dateBytes.length; + System.arraycopy(JSON_MID, 0, json, j, JSON_MID.length); j += JSON_MID.length; + System.arraycopy(JSON_BODY, 0, json, j, JSON_BODY.length); + + plaintextResponse = plain; + jsonResponse = json; + } + } +} diff --git a/frameworks/Java/netty-fast/src/main/java/hello/HelloServerInitializer.java b/frameworks/Java/netty-fast/src/main/java/hello/HelloServerInitializer.java new file mode 100644 index 00000000000..6ca37a5789d --- /dev/null +++ b/frameworks/Java/netty-fast/src/main/java/hello/HelloServerInitializer.java @@ -0,0 +1,20 @@ +package hello; + +import java.util.concurrent.ScheduledExecutorService; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpDecoderConfig; +import io.netty.handler.codec.http.HttpRequestDecoder; + +public class HelloServerInitializer extends ChannelInitializer { + + @Override + protected void initChannel(SocketChannel ch) { + ScheduledExecutorService service = ch.eventLoop(); + var config = new HttpDecoderConfig().setMaxInitialLineLength(4096).setMaxHeaderSize(8192).setMaxChunkSize(8192); + + ch.pipeline().addLast("httpDecoder", new HttpRequestDecoder(config)); + ch.pipeline().addLast("handler", new HelloServerHandler(service)); + } +} diff --git a/frameworks/Java/netty-fast/src/main/java/hello/HelloWebServer.java b/frameworks/Java/netty-fast/src/main/java/hello/HelloWebServer.java new file mode 100644 index 00000000000..41743e50406 --- /dev/null +++ b/frameworks/Java/netty-fast/src/main/java/hello/HelloWebServer.java @@ -0,0 +1,84 @@ +package hello; + +import java.net.InetSocketAddress; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelOption; +import io.netty.channel.epoll.EpollChannelOption; +import io.netty.channel.uring.IoUringChannelOption; +import io.netty.util.ResourceLeakDetector; +import io.netty.util.ResourceLeakDetector.Level; + +public class HelloWebServer { + + private static final IoMultiplexer PREFERRED_TRANSPORT; + + static { + ResourceLeakDetector.setLevel(Level.DISABLED); + String transportName = System.getProperty("hello.transport"); + if (transportName != null) { + try { + PREFERRED_TRANSPORT = IoMultiplexer.valueOf(transportName); + } catch (IllegalArgumentException e) { + System.err.println("Invalid transport name: " + transportName); + throw e; + } + } else { + PREFERRED_TRANSPORT = IoMultiplexer.type(); + } + } + + private final int port; + + public HelloWebServer(int port) { + this.port = port; + } + + public void run() throws Exception { + final var preferredTransport = PREFERRED_TRANSPORT; + System.out.printf("Using %s IoMultiplexer%n", preferredTransport); + final int coreCount = Runtime.getRuntime().availableProcessors(); + final var group = preferredTransport.newEventLoopGroup(coreCount); + + try { + final var serverChannelClass = preferredTransport.serverChannelClass(); + var inet = new InetSocketAddress(port); + var b = new ServerBootstrap(); + + b.option(ChannelOption.SO_BACKLOG, 8192); + b.option(ChannelOption.SO_REUSEADDR, true); + switch (preferredTransport) { + case EPOLL: + b.option(EpollChannelOption.SO_REUSEPORT, true); + break; + case IO_URING: + b.option(IoUringChannelOption.SO_REUSEPORT, true); + break; + } + var channelB = b.group(group).channel(serverChannelClass); + channelB.childHandler(new HelloServerInitializer()); + b.childOption(ChannelOption.SO_REUSEADDR, true); + + Channel ch = b.bind(inet).sync().channel(); + + System.out.printf("Httpd started. Listening on: %s%n", inet); + + ch.closeFuture().sync(); + } finally { + group.shutdownGracefully().sync(); + } + } + + public static void main(String[] args) throws Exception { + int port; + if (args.length > 0) { + port = Integer.parseInt(args[0]); + } else { + port = 8080; + } + new HelloWebServer(port).run(); + + + } +} diff --git a/frameworks/Java/netty-fast/src/main/java/hello/IoMultiplexer.java b/frameworks/Java/netty-fast/src/main/java/hello/IoMultiplexer.java new file mode 100644 index 00000000000..5797bbe14f5 --- /dev/null +++ b/frameworks/Java/netty-fast/src/main/java/hello/IoMultiplexer.java @@ -0,0 +1,54 @@ +package hello; + +import io.netty.channel.IoHandlerFactory; +import io.netty.channel.MultiThreadIoEventLoopGroup; +import io.netty.channel.ServerChannel; +import io.netty.channel.epoll.Epoll; +import io.netty.channel.epoll.EpollIoHandler; +import io.netty.channel.epoll.EpollServerSocketChannel; +import io.netty.channel.kqueue.KQueue; +import io.netty.channel.kqueue.KQueueIoHandler; +import io.netty.channel.kqueue.KQueueServerSocketChannel; +import io.netty.channel.nio.NioIoHandler; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.channel.uring.IoUring; +import io.netty.channel.uring.IoUringIoHandler; +import io.netty.channel.uring.IoUringServerSocketChannel; + +public enum IoMultiplexer { + EPOLL, KQUEUE, JDK, IO_URING; + + public Class serverChannelClass() { + return switch (this) { + case EPOLL -> EpollServerSocketChannel.class; + case KQUEUE -> KQueueServerSocketChannel.class; + case JDK -> NioServerSocketChannel.class; + case IO_URING -> IoUringServerSocketChannel.class; + }; + } + + public IoHandlerFactory newIoHandlerFactory() { + return switch (this) { + case EPOLL -> EpollIoHandler.newFactory(); + case KQUEUE -> KQueueIoHandler.newFactory(); + case JDK -> NioIoHandler.newFactory(); + case IO_URING -> IoUringIoHandler.newFactory(); + }; + } + + public MultiThreadIoEventLoopGroup newEventLoopGroup(int nThreads) { + return new MultiThreadIoEventLoopGroup(nThreads, newIoHandlerFactory()); + } + + public static IoMultiplexer type() { + if (IoUring.isAvailable()) { + return IO_URING; + } else if (Epoll.isAvailable()) { + return EPOLL; + } else if (KQueue.isAvailable()) { + return KQUEUE; + } else { + return JDK; + } + } +} \ No newline at end of file diff --git a/frameworks/Java/netty-fast/src/main/java/hello/JsonUtils.java b/frameworks/Java/netty-fast/src/main/java/hello/JsonUtils.java new file mode 100644 index 00000000000..4b6db13e619 --- /dev/null +++ b/frameworks/Java/netty-fast/src/main/java/hello/JsonUtils.java @@ -0,0 +1,13 @@ +package hello; + +import com.alibaba.fastjson2.JSON; + +public final class JsonUtils { + + private JsonUtils() { + } + + public static byte[] serializeMsg(Message obj) { + return JSON.toJSONBytes(obj); + } +} diff --git a/frameworks/Java/netty-fast/src/main/java/hello/Message.java b/frameworks/Java/netty-fast/src/main/java/hello/Message.java new file mode 100644 index 00000000000..13698780e9f --- /dev/null +++ b/frameworks/Java/netty-fast/src/main/java/hello/Message.java @@ -0,0 +1,15 @@ +package hello; + +public class Message { + + private final String message; + + public Message(String message) { + super(); + this.message = message; + } + + public String getMessage() { + return message; + } +} \ No newline at end of file