diff --git a/README.md b/README.md index 01615ef..b6afa53 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ EchoWebSocketHandler http1WebSocketHandler = new EchoWebSocketHandler(); Http2WebSocketClientHandshaker handShaker = Http2WebSocketClientHandshaker.create(channel); Http2Headers headers = - new DefaultHttp2Headers().set("user-agent", "jauntsdn-websocket-http2-client/1.2.2"); + new DefaultHttp2Headers().set("user-agent", "jauntsdn-websocket-http2-client/1.2.3"); ChannelFuture handshakeFuture = /*http1 websocket handler*/ handShaker.handshake("/echo", headers, new EchoWebSocketHandler()); @@ -286,7 +286,7 @@ repositories { } dependencies { - implementation 'com.jauntsdn.netty:netty-websocket-http2:1.2.2' + implementation 'com.jauntsdn.netty:netty-websocket-http2:1.2.3' } ``` diff --git a/gradle.properties b/gradle.properties index ae346a6..d01ea95 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group=com.jauntsdn.netty -version=1.2.3 +version=1.2.4 googleJavaFormatPluginVersion=0.9 dependencyManagementPluginVersion=1.1.0 @@ -7,15 +7,15 @@ gitPluginVersion=0.13.0 osDetectorPluginVersion=1.7.3 versionsPluginVersion=0.45.0 -nettyVersion=4.1.93.Final +nettyVersion=4.1.96.Final jauntNettyWebsocketHttp1=1.1.0 nettyTcnativeVersion=2.0.61.Final hdrHistogramVersion=2.1.12 slf4jVersion=1.7.36 -logbackVersion=1.2.11 +logbackVersion=1.2.12 jsr305Version=3.0.2 -junitVersion=5.9.3 +junitVersion=5.10.0 assertjVersion=3.24.2 org.gradle.parallel=true diff --git a/netty-websocket-http2-callbacks-codec/gradle.lockfile b/netty-websocket-http2-callbacks-codec/gradle.lockfile index a2d6dd4..eb96247 100644 --- a/netty-websocket-http2-callbacks-codec/gradle.lockfile +++ b/netty-websocket-http2-callbacks-codec/gradle.lockfile @@ -8,14 +8,14 @@ com.google.googlejavaformat:google-java-format:1.6=googleJavaFormat1.6 com.google.guava:guava:22.0=googleJavaFormat1.6 com.google.j2objc:j2objc-annotations:1.1=googleJavaFormat1.6 com.jauntsdn.netty:netty-websocket-http1:1.1.0=compileClasspath -io.netty:netty-buffer:4.1.93.Final=compileClasspath -io.netty:netty-codec-http2:4.1.93.Final=compileClasspath -io.netty:netty-codec-http:4.1.93.Final=compileClasspath -io.netty:netty-codec:4.1.93.Final=compileClasspath -io.netty:netty-common:4.1.93.Final=compileClasspath -io.netty:netty-handler:4.1.93.Final=compileClasspath -io.netty:netty-resolver:4.1.93.Final=compileClasspath -io.netty:netty-transport-native-unix-common:4.1.93.Final=compileClasspath -io.netty:netty-transport:4.1.93.Final=compileClasspath +io.netty:netty-buffer:4.1.96.Final=compileClasspath +io.netty:netty-codec-http2:4.1.96.Final=compileClasspath +io.netty:netty-codec-http:4.1.96.Final=compileClasspath +io.netty:netty-codec:4.1.96.Final=compileClasspath +io.netty:netty-common:4.1.96.Final=compileClasspath +io.netty:netty-handler:4.1.96.Final=compileClasspath +io.netty:netty-resolver:4.1.96.Final=compileClasspath +io.netty:netty-transport-native-unix-common:4.1.96.Final=compileClasspath +io.netty:netty-transport:4.1.96.Final=compileClasspath org.codehaus.mojo:animal-sniffer-annotations:1.14=googleJavaFormat1.6 empty=annotationProcessor diff --git a/netty-websocket-http2-perftest/build.gradle b/netty-websocket-http2-perftest/build.gradle index 0356b11..db9c671 100644 --- a/netty-websocket-http2-perftest/build.gradle +++ b/netty-websocket-http2-perftest/build.gradle @@ -57,6 +57,16 @@ task runClientCallbacks(type: JavaExec) { mainClass = "com.jauntsdn.netty.handler.codec.http2.websocketx.perftest.callbackscodec.client.Main" } +task runServerBulk(type: JavaExec) { + classpath = sourceSets.main.runtimeClasspath + mainClass = "com.jauntsdn.netty.handler.codec.http2.websocketx.perftest.bulkcodec.server.Main" +} + +task runClientBulk(type: JavaExec) { + classpath = sourceSets.main.runtimeClasspath + mainClass = "com.jauntsdn.netty.handler.codec.http2.websocketx.perftest.bulkcodec.client.Main" +} + task serverMessagesScripts(type: CreateStartScripts) { mainClass = "com.jauntsdn.netty.handler.codec.http2.websocketx.perftest.messagecodec.server.Main" applicationName = "${project.name}-messages-server" @@ -85,10 +95,26 @@ task clientCallbacksScripts(type: CreateStartScripts) { outputDir = startScripts.outputDir } +task serverBulkScripts(type: CreateStartScripts) { + mainClass = "com.jauntsdn.netty.handler.codec.http2.websocketx.perftest.bulkcodec.server.Main" + applicationName = "${project.name}-bulk-server" + classpath = startScripts.classpath + outputDir = startScripts.outputDir +} + +task clientBulkScripts(type: CreateStartScripts) { + mainClass = "com.jauntsdn.netty.handler.codec.http2.websocketx.perftest.bulkcodec.client.Main" + applicationName = "${project.name}-bulk-client" + classpath = startScripts.classpath + outputDir = startScripts.outputDir +} + startScripts.dependsOn serverMessagesScripts startScripts.dependsOn clientMessagesScripts startScripts.dependsOn serverCallbacksScripts startScripts.dependsOn clientCallbacksScripts +startScripts.dependsOn serverBulkScripts +startScripts.dependsOn clientBulkScripts tasks.named("startScripts") { enabled = false diff --git a/netty-websocket-http2-perftest/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/perftest/bulkcodec/client/Main.java b/netty-websocket-http2-perftest/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/perftest/bulkcodec/client/Main.java new file mode 100644 index 0000000..1362951 --- /dev/null +++ b/netty-websocket-http2-perftest/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/perftest/bulkcodec/client/Main.java @@ -0,0 +1,476 @@ +/* + * Copyright 2022 - present Maksym Ostroverkhov. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jauntsdn.netty.handler.codec.http2.websocketx.perftest.bulkcodec.client; + +import static com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Http2WebSocketLifecycleEvent; +import static com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Type; + +import com.jauntsdn.netty.handler.codec.http.websocketx.WebSocketCallbacksHandler; +import com.jauntsdn.netty.handler.codec.http.websocketx.WebSocketFrameFactory; +import com.jauntsdn.netty.handler.codec.http.websocketx.WebSocketFrameListener; +import com.jauntsdn.netty.handler.codec.http.websocketx.WebSocketProtocol; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketClientBuilder; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketClientHandler; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketClientHandshaker; +import com.jauntsdn.netty.handler.codec.http2.websocketx.WebSocketCallbacksCodec; +import com.jauntsdn.netty.handler.codec.http2.websocketx.perftest.Security; +import com.jauntsdn.netty.handler.codec.http2.websocketx.perftest.Transport; +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.epoll.Epoll; +import io.netty.channel.kqueue.KQueue; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.websocketx.WebSocketDecoderConfig; +import io.netty.handler.codec.http2.Http2FrameCodec; +import io.netty.handler.codec.http2.Http2FrameCodecBuilder; +import io.netty.handler.ssl.OpenSsl; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslHandler; +import io.netty.util.concurrent.GenericFutureListener; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import org.HdrHistogram.Histogram; +import org.HdrHistogram.Recorder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Main { + private static final Logger logger = LoggerFactory.getLogger(Main.class); + + public static void main(String[] args) throws Exception { + String host = System.getProperty("HOST", "localhost"); + int port = Integer.parseInt(System.getProperty("PORT", "8088")); + int duration = Integer.parseInt(System.getProperty("DURATION", "600")); + boolean isNativeTransport = Boolean.parseBoolean(System.getProperty("NATIVE", "true")); + int flowControlWindowSize = Integer.parseInt(System.getProperty("WINDOW", "100000")); + int frameSize = Integer.parseInt(System.getProperty("FRAME", "140")); + int outboundFramesWindow = Integer.parseInt(System.getProperty("WINDOW", "2222")); + + boolean isOpensslAvailable = OpenSsl.isAvailable(); + boolean isEpollAvailable = Epoll.isAvailable(); + boolean isKqueueAvailable = KQueue.isAvailable(); + + logger.info("\n==> http2 websocket bulk codec perf test client\n"); + logger.info("\n==> remote address: {}:{}", host, port); + logger.info("\n==> duration: {}", duration); + logger.info("\n==> native transport: {}", isNativeTransport); + logger.info("\n==> epoll available: {}", isEpollAvailable); + logger.info("\n==> kqueue available: {}", isKqueueAvailable); + logger.info("\n==> openssl available: {}\n", isOpensslAvailable); + logger.info("\n==> frame payload size: {}", frameSize); + logger.info("\n==> outbound frames window: {}", outboundFramesWindow); + + Transport transport = Transport.get(isNativeTransport); + + final SslContext sslContext = Security.clientLocalSslContext(); + + Channel channel = + new Bootstrap() + .group(transport.eventLoopGroup()) + .channel(transport.clientChannel()) + .handler( + new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) { + SslHandler sslHandler = sslContext.newHandler(ch.alloc()); + + Http2FrameCodecBuilder frameCodecBuilder = Http2FrameCodecBuilder.forClient(); + frameCodecBuilder.initialSettings().initialWindowSize(flowControlWindowSize); + Http2FrameCodec http2FrameCodec = frameCodecBuilder.build(); + + WebSocketDecoderConfig decoderConfig = + WebSocketDecoderConfig.newBuilder() + .maxFramePayloadLength(65_535) + .expectMaskedFrames(false) + .allowMaskMismatch(true) + .allowExtensions(false) + .withUTF8Validator(false) + .build(); + + Http2WebSocketClientHandler http2WebSocketClientHandler = + Http2WebSocketClientBuilder.create() + .codec(WebSocketCallbacksCodec.instance()) + .compression(false) + .decoderConfig(decoderConfig) + .maskPayload(false) + .handshakeTimeoutMillis(15_000) + .assumeSingleWebSocketPerConnection(true) + .build(); + ch.pipeline().addLast(sslHandler, http2FrameCodec, http2WebSocketClientHandler); + } + }) + .connect(new InetSocketAddress(host, port)) + .sync() + .channel(); + Http2WebSocketClientHandshaker handShaker = Http2WebSocketClientHandshaker.create(channel); + + Random random = new Random(); + List framesPayload = framesPayload(1000, frameSize, random); + FrameCounters frameCounters = new FrameCounters(false); + + Recorder framesHistogram = new Recorder(3600000000000L, 3); + + ChannelFuture handshakeFuture = + handShaker.handshake( + "/echo", + new WebSocketsCallbacksHandler( + new WebSocketClientHandler( + frameCounters, + framesPayload, + ThreadLocalRandom.current(), + outboundFramesWindow))); + + handshakeFuture.addListener(new CloseOnError(channel)); + + int warmupMillis = 5000; + logger.info("==> warming up for {} millis...", warmupMillis); + channel + .eventLoop() + .schedule( + () -> { + logger.info("==> warm up completed"); + frameCounters.start(); + channel + .eventLoop() + .scheduleAtFixedRate( + new StatsReporter(frameCounters, frameSize), + 1000, + 1000, + TimeUnit.MILLISECONDS); + }, + warmupMillis, + TimeUnit.MILLISECONDS); + + channel.closeFuture().sync(); + logger.info("Client terminated"); + } + + private static class FrameCounters { + private final Recorder histogram; + private int frameCount; + private boolean isStarted; + + public FrameCounters(boolean totalFrames) { + histogram = totalFrames ? null : new Recorder(36000000000L, 3); + } + + private long totalFrameCount; + + public void start() { + isStarted = true; + } + + public void countFrame(long timestamp) { + if (!isStarted) { + return; + } + + if (histogram == null) { + totalFrameCount++; + } else { + frameCount++; + if (timestamp >= 0) { + histogram.recordValue(System.nanoTime() - timestamp); + } + } + } + + public Recorder histogram() { + return histogram; + } + + public int frameCount() { + int count = frameCount; + frameCount = 0; + return count; + } + + public long totalFrameCount() { + return totalFrameCount; + } + } + + private static class StatsReporter implements Runnable { + private final FrameCounters frameCounters; + private final int frameSize; + private int iteration; + + public StatsReporter(FrameCounters frameCounters, int frameSize) { + this.frameCounters = frameCounters; + this.frameSize = frameSize; + } + + @Override + public void run() { + Recorder histogram = frameCounters.histogram(); + if (histogram != null) { + Histogram h = histogram.getIntervalHistogram(); + long p50 = h.getValueAtPercentile(50) / 1000; + long p95 = h.getValueAtPercentile(95) / 1000; + long p99 = h.getValueAtPercentile(99) / 1000; + int count = frameCounters.frameCount(); + + logger.info("p50 => {} micros", p50); + logger.info("p95 => {} micros", p95); + logger.info("p99 => {} micros", p99); + logger.info("throughput => {} messages", count); + logger.info("throughput => {} kbytes\n", count * frameSize / (float) 1024); + } else { + if (++iteration % 10 == 0) { + logger.info( + "total frames, iteration {} => {}", iteration, frameCounters.totalFrameCount()); + } + } + } + } + + private static class CloseOnError implements GenericFutureListener { + private final Channel connection; + + public CloseOnError(Channel connection) { + this.connection = connection; + } + + @Override + public void operationComplete(ChannelFuture future) { + Throwable cause = future.cause(); + if (cause != null) { + logger.info( + "Websocket handshake error: {}:{}", + cause.getClass().getSimpleName(), + cause.getMessage()); + connection.eventLoop().shutdownGracefully(); + } + } + } + + static class WebSocketClientHandler implements WebSocketCallbacksHandler, WebSocketFrameListener { + + private final FrameCounters frameCounters; + private final List dataList; + private final Random random; + private final int window; + private int sendIndex; + private boolean isClosed; + private FrameWriter frameWriter; + + WebSocketClientHandler( + FrameCounters frameCounters, List dataList, Random random, int window) { + this.frameCounters = frameCounters; + this.dataList = dataList; + this.random = random; + this.window = window; + } + + @Override + public WebSocketFrameListener exchange( + ChannelHandlerContext ctx, WebSocketFrameFactory frameFactory) { + frameWriter = new FrameWriter(ctx, frameFactory.bulkEncoder(), window); + return this; + } + + @Override + public void onOpen(ChannelHandlerContext ctx) { + frameWriter.startWrite(); + } + + @Override + public void onClose(ChannelHandlerContext ctx) { + isClosed = true; + frameWriter.close(); + } + + @Override + public void onChannelRead( + ChannelHandlerContext ctx, boolean finalFragment, int rsv, int opcode, ByteBuf payload) { + if (opcode != WebSocketProtocol.OPCODE_BINARY) { + payload.release(); + return; + } + + long timeStamp = payload.readLong(); + frameCounters.countFrame(timeStamp); + payload.release(); + frameWriter.tryContinueWrite(); + } + + @Override + public void onChannelReadComplete(ChannelHandlerContext ctx) {} + + @Override + public void onUserEventTriggered(ChannelHandlerContext ctx, Object evt) {} + + @Override + public void onChannelWritabilityChanged(ChannelHandlerContext ctx) { + Channel ch = ctx.channel(); + if (!ch.isWritable()) { + ch.flush(); + } + } + + @Override + public void onExceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + if (!isClosed) { + isClosed = true; + logger.error("Channel error", cause); + ctx.close(); + } + } + + class FrameWriter { + private final ChannelHandlerContext ctx; + private final WebSocketFrameFactory.BulkEncoder bulkEncoder; + private ByteBuf outbuffer; + private final int window; + private int queued; + + FrameWriter( + ChannelHandlerContext ctx, WebSocketFrameFactory.BulkEncoder bulkEncoder, int window) { + this.ctx = ctx; + this.bulkEncoder = bulkEncoder; + this.window = window; + this.outbuffer = ctx.alloc().buffer(4096, 4096); + } + + void close() { + ByteBuf out = outbuffer; + if (out != null) { + outbuffer = null; + out.release(); + } + } + + void startWrite() { + if (isClosed) { + return; + } + int cur = queued; + int w = window; + int writeCount = w - cur; + ChannelHandlerContext c = ctx; + for (int i = 0; i < writeCount; i++) { + writeWebSocketFrame(c); + } + queued = w; + ByteBuf out = outbuffer; + if (out.readableBytes() > 0) { + c.write(out, c.voidPromise()); + outbuffer = ctx.alloc().buffer(4096, 4096); + } + c.flush(); + } + + void tryContinueWrite() { + int q = --queued; + if (q <= window / 2) { + startWrite(); + } + } + + void writeWebSocketFrame(ChannelHandlerContext ctx) { + List dl = dataList; + int dataIndex = random.nextInt(dl.size()); + ByteBuf data = dl.get(dataIndex); + int index = sendIndex++; + int dataSize = data.readableBytes(); + int payloadSize = Long.BYTES + dataSize; + + ByteBuf out = outbuffer; + WebSocketFrameFactory.BulkEncoder encoder = bulkEncoder; + int outSize = encoder.sizeofBinaryFrame(payloadSize); + if (outSize > out.capacity() - out.writerIndex()) { + ctx.write(out, ctx.voidPromise()); + out = outbuffer = ctx.alloc().buffer(4096, 4096); + } + + int mask = encoder.encodeBinaryFramePrefix(out, payloadSize); + out.writeLong(index % 50_000 == 0 ? System.nanoTime() : -1).writeBytes(data, 0, dataSize); + encoder.maskBinaryFrame(out, mask, payloadSize); + } + } + } + + private static class WebSocketsCallbacksHandler extends ChannelInboundHandlerAdapter { + final WebSocketCallbacksHandler webSocketHandler; + + WebSocketsCallbacksHandler(WebSocketCallbacksHandler webSocketHandler) { + this.webSocketHandler = webSocketHandler; + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof Http2WebSocketLifecycleEvent) { + Http2WebSocketLifecycleEvent handshakeEvent = (Http2WebSocketLifecycleEvent) evt; + Type eventType = handshakeEvent.type(); + switch (eventType) { + case HANDSHAKE_START: + case CLOSE_REMOTE_ENDSTREAM: + case CLOSE_REMOTE_RESET: + break; + case HANDSHAKE_SUCCESS: + logger.info("==> WebSocket handshake success"); + WebSocketCallbacksHandler.exchange(ctx, webSocketHandler); + ctx.pipeline().remove(this); + break; + case HANDSHAKE_ERROR: + logger.info("==> WebSocket handshake error"); + break; + default: + logger.info("==> WebSocket handshake unexpected event - type: {}", eventType); + } + return; + } + super.userEventTriggered(ctx, evt); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + if (cause instanceof IOException) { + return; + } + logger.info("Unexpected websocket error", cause); + ctx.close(); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + logger.info("Received {} message on callbacks handler", msg); + super.channelRead(ctx, msg); + } + } + + private static List framesPayload(int count, int size, Random random) { + List data = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + byte[] bytes = new byte[size]; + random.nextBytes(bytes); + data.add(Unpooled.wrappedBuffer(bytes)); + } + return data; + } +} diff --git a/netty-websocket-http2-perftest/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/perftest/bulkcodec/server/Main.java b/netty-websocket-http2-perftest/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/perftest/bulkcodec/server/Main.java new file mode 100644 index 0000000..4501de4 --- /dev/null +++ b/netty-websocket-http2-perftest/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/perftest/bulkcodec/server/Main.java @@ -0,0 +1,264 @@ +/* + * Copyright 2023 - present Maksym Ostroverkhov. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jauntsdn.netty.handler.codec.http2.websocketx.perftest.bulkcodec.server; + +import com.jauntsdn.netty.handler.codec.http.websocketx.WebSocketCallbacksHandler; +import com.jauntsdn.netty.handler.codec.http.websocketx.WebSocketFrameFactory; +import com.jauntsdn.netty.handler.codec.http.websocketx.WebSocketFrameListener; +import com.jauntsdn.netty.handler.codec.http.websocketx.WebSocketProtocol; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketServerBuilder; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketServerHandler; +import com.jauntsdn.netty.handler.codec.http2.websocketx.WebSocketCallbacksCodec; +import com.jauntsdn.netty.handler.codec.http2.websocketx.perftest.Security; +import com.jauntsdn.netty.handler.codec.http2.websocketx.perftest.Transport; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.epoll.Epoll; +import io.netty.channel.kqueue.KQueue; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.websocketx.WebSocketDecoderConfig; +import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException; +import io.netty.handler.codec.http2.DefaultHttp2RemoteFlowController; +import io.netty.handler.codec.http2.Http2Connection; +import io.netty.handler.codec.http2.Http2FrameCodec; +import io.netty.handler.codec.http2.Http2FrameCodecBuilder; +import io.netty.handler.codec.http2.UniformStreamByteDistributor; +import io.netty.handler.ssl.OpenSsl; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslHandler; +import java.io.IOException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Main { + private static final Logger logger = LoggerFactory.getLogger(Main.class); + + public static void main(String[] args) throws Exception { + + String host = System.getProperty("HOST", "localhost"); + int port = Integer.parseInt(System.getProperty("PORT", "8088")); + boolean isNativeTransport = + Boolean.parseBoolean(System.getProperty("NATIVE_TRANSPORT", "true")); + int flowControlWindowSize = + Integer.parseInt(System.getProperty("FLOW_CONTROL_WINDOW", "100000")); + boolean isOpensslAvailable = OpenSsl.isAvailable(); + boolean isEpollAvailable = Epoll.isAvailable(); + boolean isKqueueAvailable = KQueue.isAvailable(); + + logger.info("\n==> http2 websocket bulk codec perf test server\n"); + logger.info("\n==> bind address: {}:{}", host, port); + logger.info("\n==> flow control window size: {}", flowControlWindowSize); + logger.info("\n==> native transport: {}", isNativeTransport); + logger.info("\n==> epoll available: {}", isEpollAvailable); + logger.info("\n==> kqueue available: {}", isKqueueAvailable); + logger.info("\n==> openssl available: {}", isOpensslAvailable); + + Transport transport = Transport.get(isNativeTransport); + + SslContext sslContext = Security.serverSslContext(); + + ServerBootstrap bootstrap = new ServerBootstrap(); + Channel server = + bootstrap + .group(transport.eventLoopGroup()) + .channel(transport.serverChannel()) + .childHandler(new ConnectionAcceptor(flowControlWindowSize, sslContext)) + .bind(host, port) + .sync() + .channel(); + logger.info("\n==> Server is listening on {}:{}", host, port); + server.closeFuture().sync(); + } + + private static class ConnectionAcceptor extends ChannelInitializer { + private final int flowControlWindowSize; + private final SslContext sslContext; + + ConnectionAcceptor(int flowControlWindowSize, SslContext sslContext) { + this.flowControlWindowSize = flowControlWindowSize; + this.sslContext = sslContext; + } + + @Override + protected void initChannel(SocketChannel ch) { + SslHandler sslHandler = sslContext.newHandler(ch.alloc()); + + Http2FrameCodecBuilder http2Builder = + Http2WebSocketServerBuilder.configureHttp2Server(Http2FrameCodecBuilder.forServer()); + http2Builder.initialSettings().initialWindowSize(flowControlWindowSize); + Http2FrameCodec http2FrameCodec = http2Builder.build(); + Http2Connection connection = http2FrameCodec.connection(); + connection + .remote() + .flowController( + new DefaultHttp2RemoteFlowController( + connection, new UniformStreamByteDistributor(connection))); + + WebSocketsCallbacksHandler webSocketsCallbacksHandler = + new WebSocketsCallbacksHandler(new EchoWebSocketHandler()); + + WebSocketDecoderConfig decoderConfig = + WebSocketDecoderConfig.newBuilder() + .maxFramePayloadLength(65_535) + .expectMaskedFrames(false) + .allowMaskMismatch(true) + .allowExtensions(false) + .withUTF8Validator(false) + .build(); + + Http2WebSocketServerHandler http2webSocketHandler = + Http2WebSocketServerBuilder.create() + .codec(WebSocketCallbacksCodec.instance()) + .assumeSingleWebSocketPerConnection(true) + .compression(false) + .decoderConfig(decoderConfig) + .acceptor( + (ctx, path, subprotocols, request, response) -> { + if ("/echo".equals(path) && subprotocols.isEmpty()) { + return ctx.executor().newSucceededFuture(webSocketsCallbacksHandler); + } + return ctx.executor() + .newFailedFuture( + new WebSocketHandshakeException( + String.format( + "path not found: %s, subprotocols: %s", path, subprotocols))); + }) + .build(); + + ExceptionHandler exceptionHandler = new ExceptionHandler(); + + ch.pipeline().addLast(sslHandler, http2FrameCodec, http2webSocketHandler, exceptionHandler); + } + } + + private static class ExceptionHandler extends ChannelInboundHandlerAdapter { + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + if (cause instanceof IOException) { + return; + } + logger.error("Unexpected connection error", cause); + ctx.close(); + } + } + + private static class EchoWebSocketHandler + implements WebSocketCallbacksHandler, WebSocketFrameListener { + private WebSocketFrameFactory.BulkEncoder bulkEncoder; + private ByteBuf outbuffer; + + @Override + public WebSocketFrameListener exchange( + ChannelHandlerContext ctx, WebSocketFrameFactory webSocketFrameFactory) { + this.bulkEncoder = webSocketFrameFactory.bulkEncoder(); + return this; + } + + @Override + public void onOpen(ChannelHandlerContext ctx) { + outbuffer = ctx.alloc().buffer(4096, 4096); + } + + @Override + public void onChannelRead( + ChannelHandlerContext ctx, boolean finalFragment, int rsv, int opcode, ByteBuf payload) { + if (opcode != WebSocketProtocol.OPCODE_BINARY) { + payload.release(); + throw new IllegalStateException("received non-binary opcode"); + } + WebSocketFrameFactory.BulkEncoder encoder = bulkEncoder; + ByteBuf out = outbuffer; + int payloadSize = payload.readableBytes(); + + int outSize = encoder.sizeofBinaryFrame(payloadSize); + if (outSize > out.capacity() - out.writerIndex()) { + ctx.write(out, ctx.voidPromise()); + out = outbuffer = ctx.alloc().buffer(4096, 4096); + } + + int mask = encoder.encodeBinaryFramePrefix(out, payloadSize); + out.writeBytes(payload); + payload.release(); + encoder.maskBinaryFrame(out, mask, payloadSize); + } + + @Override + public void onChannelReadComplete(ChannelHandlerContext ctx) { + ByteBuf out = outbuffer; + if (out.readableBytes() > 0) { + ctx.writeAndFlush(out, ctx.voidPromise()); + this.outbuffer = ctx.alloc().buffer(4096, 4096); + } + } + + @Override + public void onExceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + if (cause instanceof IOException) { + return; + } + logger.info("Unexpected websocket error", cause); + ctx.close(); + } + + @Override + public void onChannelWritabilityChanged(ChannelHandlerContext ctx) { + if (!ctx.channel().isWritable()) { + ctx.flush(); + } + } + } + + @ChannelHandler.Sharable + private static class WebSocketsCallbacksHandler extends ChannelInboundHandlerAdapter { + final WebSocketCallbacksHandler webSocketHandler; + + WebSocketsCallbacksHandler(WebSocketCallbacksHandler webSocketHandler) { + this.webSocketHandler = webSocketHandler; + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof Http2WebSocketEvent.Http2WebSocketHandshakeSuccessEvent) { + + WebSocketCallbacksHandler.exchange(ctx, webSocketHandler); + ctx.pipeline().remove(this); + } + super.userEventTriggered(ctx, evt); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + logger.info("Received {} message on callbacks handler", msg); + super.channelRead(ctx, msg); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + if (cause instanceof IOException) { + return; + } + logger.info("Unexpected websocket error", cause); + ctx.close(); + } + } +} diff --git a/netty-websocket-http2/gradle.lockfile b/netty-websocket-http2/gradle.lockfile index 988f18a..615633d 100644 --- a/netty-websocket-http2/gradle.lockfile +++ b/netty-websocket-http2/gradle.lockfile @@ -1,35 +1,35 @@ # This is a Gradle generated file for dependency locking. # Manual edits can break the build and are not advised. # This file is expected to be part of source control. -ch.qos.logback:logback-classic:1.2.11=testRuntimeClasspath -ch.qos.logback:logback-core:1.2.11=testRuntimeClasspath +ch.qos.logback:logback-classic:1.2.12=testRuntimeClasspath +ch.qos.logback:logback-core:1.2.12=testRuntimeClasspath com.google.code.findbugs:jsr305:3.0.2=compileClasspath,googleJavaFormat1.6 com.google.errorprone:error_prone_annotations:2.0.18=googleJavaFormat1.6 com.google.errorprone:javac-shaded:9+181-r4173-1=googleJavaFormat1.6 com.google.googlejavaformat:google-java-format:1.6=googleJavaFormat1.6 com.google.guava:guava:22.0=googleJavaFormat1.6 com.google.j2objc:j2objc-annotations:1.1=googleJavaFormat1.6 -io.netty:netty-buffer:4.1.93.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-codec-http2:4.1.93.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-codec-http:4.1.93.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-codec:4.1.93.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-common:4.1.93.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-handler:4.1.93.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-resolver:4.1.93.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-buffer:4.1.96.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-codec-http2:4.1.96.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-codec-http:4.1.96.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-codec:4.1.96.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-common:4.1.96.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-handler:4.1.96.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-resolver:4.1.96.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.netty:netty-tcnative-boringssl-static:2.0.61.Final=testRuntimeClasspath io.netty:netty-tcnative-classes:2.0.61.Final=testRuntimeClasspath -io.netty:netty-transport-native-unix-common:4.1.93.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-transport:4.1.93.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-transport-native-unix-common:4.1.96.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-transport:4.1.96.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy:1.12.21=testCompileClasspath,testRuntimeClasspath org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath org.assertj:assertj-core:3.24.2=testCompileClasspath,testRuntimeClasspath org.codehaus.mojo:animal-sniffer-annotations:1.14=googleJavaFormat1.6 -org.junit.jupiter:junit-jupiter-api:5.9.3=testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-engine:5.9.3=testRuntimeClasspath -org.junit.jupiter:junit-jupiter-params:5.9.3=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-commons:1.9.3=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-engine:1.9.3=testRuntimeClasspath -org.junit:junit-bom:5.9.3=testCompileClasspath,testRuntimeClasspath -org.opentest4j:opentest4j:1.2.0=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.10.0=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.10.0=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.10.0=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.10.0=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.10.0=testRuntimeClasspath +org.junit:junit-bom:5.10.0=testCompileClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath org.slf4j:slf4j-api:1.7.36=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath empty=annotationProcessor,testAnnotationProcessor diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketChannelHandler.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketChannelHandler.java index 92bfbac..2b522eb 100644 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketChannelHandler.java +++ b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketChannelHandler.java @@ -27,8 +27,12 @@ import io.netty.util.concurrent.ScheduledFuture; import java.nio.channels.ClosedChannelException; import java.util.ArrayDeque; +import java.util.Collection; +import java.util.Map; import java.util.Queue; +import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; import java.util.function.Supplier; import javax.annotation.Nullable; @@ -298,7 +302,7 @@ public void run() { static Supplier> webSocketRegistryFactory( boolean isSingleWebSocketPerConnection) { if (isSingleWebSocketPerConnection) { - return () -> new Http2WebSocketHandlerContainers.SingleElementOptimizedMap<>(); + return () -> new SingleElementOptimizedMap<>(); } else { return () -> new IntObjectHashMap<>(4); } @@ -423,4 +427,170 @@ void processPendingReadCompleteQueue() { } } } + + static final class SingleElementOptimizedMap implements IntObjectMap { + /* 0: empty + * -1: delegated */ + int singleKey; + T singleValue; + IntObjectMap delegate = IntCollections.emptyMap(); + + @Override + public T get(int key) { + int sk = singleKey; + if (key == sk) { + return singleValue; + } + if (sk == -1) { + return delegate.get(key); + } + return null; + } + + @Override + public T put(int key, T value) { + int sk = singleKey; + /*empty or replace*/ + if (sk == 0 || key == sk) { + T sv = singleValue; + singleKey = key; + singleValue = value; + return sv; + } + /*put while nonEmpty - delegate*/ + IntObjectMap d = delegate; + if (d.isEmpty()) { + d = delegate = new IntObjectHashMap<>(4); + d.put(sk, singleValue); + singleKey = -1; + singleValue = null; + } + return d.put(key, value); + } + + @Override + public T remove(int key) { + int sk = singleKey; + if (key == sk) { + T sv = singleValue; + singleKey = 0; + singleValue = null; + return sv; + } + /*delegated, so not empty*/ + if (sk == -1) { + IntObjectMap d = delegate; + T removed = d.remove(key); + if (d.isEmpty()) { + singleKey = 0; + delegate = IntCollections.emptyMap(); + } + return removed; + } + /*either single key does not match, or empty*/ + return null; + } + + @Override + public boolean containsKey(int key) { + int sk = singleKey; + return sk == key || sk == -1 && delegate.containsKey(key); + } + + @Override + public int size() { + int sk = singleKey; + switch (sk) { + case 0: + return 0; + case -1: + return delegate.size(); + /*sk > 0*/ + default: + return 1; + } + } + + @Override + public boolean isEmpty() { + return singleKey == 0; + } + + @Override + public void clear() { + singleKey = 0; + singleValue = null; + delegate = IntCollections.emptyMap(); + } + + @Override + public void forEach(BiConsumer action) { + int sk = singleKey; + if (sk > 0) { + action.accept(sk, singleValue); + } else if (sk == -1) { + delegate.forEach(action); + } + } + + @Override + public Iterable> entries() { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public boolean containsKey(Object key) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public boolean containsValue(Object value) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public T get(Object key) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public T put(Integer key, T value) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public T remove(Object key) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public void putAll(Map m) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public Set keySet() { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public Collection values() { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public Set> entrySet() { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public boolean equals(Object o) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public int hashCode() { + throw new UnsupportedOperationException("Not implemented"); + } + } } diff --git a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketHandlerContainers.java b/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketHandlerContainers.java deleted file mode 100644 index 4507739..0000000 --- a/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http2WebSocketHandlerContainers.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Copyright 2020 - present Maksym Ostroverkhov. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.jauntsdn.netty.handler.codec.http2.websocketx; - -import io.netty.util.collection.IntCollections; -import io.netty.util.collection.IntObjectHashMap; -import io.netty.util.collection.IntObjectMap; -import java.util.Collection; -import java.util.Map; -import java.util.Set; -import java.util.function.BiConsumer; - -final class Http2WebSocketHandlerContainers { - - static final class SingleElementOptimizedMap implements IntObjectMap { - /* 0: empty - * -1: delegated */ - int singleKey; - T singleValue; - IntObjectMap delegate = IntCollections.emptyMap(); - - @Override - public T get(int key) { - int sk = singleKey; - if (key == sk) { - return singleValue; - } - if (sk == -1) { - return delegate.get(key); - } - return null; - } - - @Override - public T put(int key, T value) { - int sk = singleKey; - /*empty or replace*/ - if (sk == 0 || key == sk) { - T sv = singleValue; - singleKey = key; - singleValue = value; - return sv; - } - /*put while nonEmpty - delegate*/ - IntObjectMap d = delegate; - if (d.isEmpty()) { - d = delegate = new IntObjectHashMap<>(4); - d.put(sk, singleValue); - singleKey = -1; - singleValue = null; - } - return d.put(key, value); - } - - @Override - public T remove(int key) { - int sk = singleKey; - if (key == sk) { - T sv = singleValue; - singleKey = 0; - singleValue = null; - return sv; - } - /*delegated, so not empty*/ - if (sk == -1) { - IntObjectMap d = delegate; - T removed = d.remove(key); - if (d.isEmpty()) { - singleKey = 0; - delegate = IntCollections.emptyMap(); - } - return removed; - } - /*either single key does not match, or empty*/ - return null; - } - - @Override - public boolean containsKey(int key) { - int sk = singleKey; - return sk == key || sk == -1 && delegate.containsKey(key); - } - - @Override - public int size() { - int sk = singleKey; - switch (sk) { - case 0: - return 0; - case -1: - return delegate.size(); - /*sk > 0*/ - default: - return 1; - } - } - - @Override - public boolean isEmpty() { - return singleKey == 0; - } - - @Override - public void clear() { - singleKey = 0; - singleValue = null; - delegate = IntCollections.emptyMap(); - } - - @Override - public void forEach(BiConsumer action) { - int sk = singleKey; - if (sk > 0) { - action.accept(sk, singleValue); - } else if (sk == -1) { - delegate.forEach(action); - } - } - - @Override - public Iterable> entries() { - throw new UnsupportedOperationException("Not implemented"); - } - - @Override - public boolean containsKey(Object key) { - throw new UnsupportedOperationException("Not implemented"); - } - - @Override - public boolean containsValue(Object value) { - throw new UnsupportedOperationException("Not implemented"); - } - - @Override - public T get(Object key) { - throw new UnsupportedOperationException("Not implemented"); - } - - @Override - public T put(Integer key, T value) { - throw new UnsupportedOperationException("Not implemented"); - } - - @Override - public T remove(Object key) { - throw new UnsupportedOperationException("Not implemented"); - } - - @Override - public void putAll(Map m) { - throw new UnsupportedOperationException("Not implemented"); - } - - @Override - public Set keySet() { - throw new UnsupportedOperationException("Not implemented"); - } - - @Override - public Collection values() { - throw new UnsupportedOperationException("Not implemented"); - } - - @Override - public Set> entrySet() { - throw new UnsupportedOperationException("Not implemented"); - } - - @Override - public boolean equals(Object o) { - throw new UnsupportedOperationException("Not implemented"); - } - - @Override - public int hashCode() { - throw new UnsupportedOperationException("Not implemented"); - } - } -} diff --git a/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/SingleElementOptimizedMapTest.java b/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/SingleElementOptimizedMapTest.java index 2f014f8..4d7f6ff 100644 --- a/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/SingleElementOptimizedMapTest.java +++ b/netty-websocket-http2/src/test/java/com/jauntsdn/netty/handler/codec/http2/websocketx/SingleElementOptimizedMapTest.java @@ -16,6 +16,7 @@ package com.jauntsdn.netty.handler.codec.http2.websocketx; +import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketChannelHandler.SingleElementOptimizedMap; import io.netty.util.collection.IntCollections; import io.netty.util.collection.IntObjectHashMap; import java.util.ArrayList; @@ -32,8 +33,7 @@ void putSingleElement() { int expectedKey = 42; String expectedValue = "first"; - Http2WebSocketHandlerContainers.SingleElementOptimizedMap map = - new Http2WebSocketHandlerContainers.SingleElementOptimizedMap<>(); + SingleElementOptimizedMap map = new SingleElementOptimizedMap<>(); map.put(expectedKey, expectedValue); Assertions.assertThat(map.get(expectedKey)).isEqualTo(expectedValue); @@ -60,8 +60,7 @@ void putSingleElement() { void removeWhileEmpty() { int key = 42; - Http2WebSocketHandlerContainers.SingleElementOptimizedMap map = - new Http2WebSocketHandlerContainers.SingleElementOptimizedMap<>(); + SingleElementOptimizedMap map = new SingleElementOptimizedMap<>(); String removed = map.remove(key); Assertions.assertThat(removed).isNull(); @@ -79,8 +78,7 @@ void removePresentSingleElement() { int expectedKey = 42; String expectedValue = "first"; - Http2WebSocketHandlerContainers.SingleElementOptimizedMap map = - new Http2WebSocketHandlerContainers.SingleElementOptimizedMap<>(); + SingleElementOptimizedMap map = new SingleElementOptimizedMap<>(); map.put(expectedKey, expectedValue); String removed = map.remove(expectedKey); @@ -105,14 +103,12 @@ void removePresentSingleElement() { Assertions.assertThat(map.delegate).isSameAs(IntCollections.emptyMap()); } - @SuppressWarnings({"ConstantConditions", "RedundantOperationOnEmptyContainer"}) @Test void removeAbsentSingleElement() { int expectedKey = 42; String expectedValue = "first"; - Http2WebSocketHandlerContainers.SingleElementOptimizedMap map = - new Http2WebSocketHandlerContainers.SingleElementOptimizedMap<>(); + SingleElementOptimizedMap map = new SingleElementOptimizedMap<>(); map.put(expectedKey, expectedValue); String removed = map.remove(0); @@ -142,8 +138,7 @@ void removeThenPutSingleElement() { int expectedKey = 42; String expectedValue = "first"; - Http2WebSocketHandlerContainers.SingleElementOptimizedMap map = - new Http2WebSocketHandlerContainers.SingleElementOptimizedMap<>(); + SingleElementOptimizedMap map = new SingleElementOptimizedMap<>(); map.put(expectedKey, expectedValue); map.remove(expectedKey); String prevValue = map.put(expectedKey, expectedValue); @@ -175,8 +170,7 @@ void replaceSingleElement() { String originalValue = "first"; String replaceValue = "second"; - Http2WebSocketHandlerContainers.SingleElementOptimizedMap map = - new Http2WebSocketHandlerContainers.SingleElementOptimizedMap<>(); + SingleElementOptimizedMap map = new SingleElementOptimizedMap<>(); map.put(expectedKey, originalValue); String replaced = map.put(expectedKey, replaceValue); @@ -208,8 +202,7 @@ void putMultipleElements() { int secondKey = 7; String secondValue = "second"; - Http2WebSocketHandlerContainers.SingleElementOptimizedMap map = - new Http2WebSocketHandlerContainers.SingleElementOptimizedMap<>(); + SingleElementOptimizedMap map = new SingleElementOptimizedMap<>(); map.put(firstKey, firstValue); map.put(secondKey, secondValue); @@ -243,8 +236,7 @@ void removeSingleAfterPutMultipleElements() { int secondKey = 7; String secondValue = "second"; - Http2WebSocketHandlerContainers.SingleElementOptimizedMap map = - new Http2WebSocketHandlerContainers.SingleElementOptimizedMap<>(); + SingleElementOptimizedMap map = new SingleElementOptimizedMap<>(); map.put(firstKey, firstValue); map.put(secondKey, secondValue); String removed = map.remove(firstKey); @@ -281,8 +273,7 @@ void removeAllAfterPutMultipleElements() { int secondKey = 7; String secondValue = "second"; - Http2WebSocketHandlerContainers.SingleElementOptimizedMap map = - new Http2WebSocketHandlerContainers.SingleElementOptimizedMap<>(); + SingleElementOptimizedMap map = new SingleElementOptimizedMap<>(); map.put(firstKey, firstValue); map.put(secondKey, secondValue); String removedFirst = map.remove(firstKey); @@ -321,8 +312,7 @@ void clear() { int secondKey = 7; String secondValue = "second"; - Http2WebSocketHandlerContainers.SingleElementOptimizedMap map = - new Http2WebSocketHandlerContainers.SingleElementOptimizedMap<>(); + SingleElementOptimizedMap map = new SingleElementOptimizedMap<>(); map.put(firstKey, firstValue); map.put(secondKey, secondValue); map.clear(); diff --git a/perf_client_bulk.sh b/perf_client_bulk.sh new file mode 100755 index 0000000..0db40a0 --- /dev/null +++ b/perf_client_bulk.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +./gradlew netty-websocket-http2-perftest:runClientBulk \ No newline at end of file diff --git a/perf_client_bulk_run.sh b/perf_client_bulk_run.sh new file mode 100755 index 0000000..97b4a25 --- /dev/null +++ b/perf_client_bulk_run.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +cd netty-websocket-http2-perftest/build/install/netty-websocket-http2-perftest/bin && ./netty-websocket-http2-perftest-bulk-client \ No newline at end of file diff --git a/perf_server_bulk.sh b/perf_server_bulk.sh new file mode 100755 index 0000000..e700d62 --- /dev/null +++ b/perf_server_bulk.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +./gradlew netty-websocket-http2-perftest:runServerBulk \ No newline at end of file diff --git a/perf_server_bulk_run.sh b/perf_server_bulk_run.sh new file mode 100755 index 0000000..cd3a007 --- /dev/null +++ b/perf_server_bulk_run.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +export NETTY_WEBSOCKET_HTTP2_PERFTEST_BULK_SERVER_OPTS='--add-exports java.base/sun.security.x509=ALL-UNNAMED' + +cd netty-websocket-http2-perftest/build/install/netty-websocket-http2-perftest/bin && ./netty-websocket-http2-perftest-bulk-server \ No newline at end of file