mirror of
https://github.com/layue13/MoonBridge.git
synced 2026-10-25 18:46:18 +00:00
perf: WebSocket 出站每次 flush 合并为一条二进制消息
- WebSocketFrameAdapter:flush 前写入的字节用 CoalescingBufferQueue 合并为一条 BinaryWebSocketFrame,最大 64 KiB。之前每个 Minecraft 帧一条消息,而 Netty 的编码器对 小于 1 KiB 的消息每条都分配并复制一次。每次写入的 promise 在承载它的消息写出时完成; 未 flush 的写入在关闭时失败并释放;排队字节计入通道可写性。入站不变 - ProxySessionBenchmark:新增 --websocket 1,代理阶段客户端走 ws:// - 记录 testing/benchmarks/results/2026-10-22-websocket-coalescing:burst32 每帧 CPU 低 21% 且范围不重叠、吞吐更高;window 1 无变化;window 16 噪声大,补跑 3 对后合计无回退。 满足事先规则,采用 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0253130760
commit
21a5adf18d
6 files changed
+414
-26
No files matched your search
@@ -86,6 +86,8 @@ sh testing/smoke/container-channel.sh
|
||||
|
||||
`-Burst N`(直接运行 Java 时为 `--burst N`)让每次往返由客户端一次写出 N 个帧、模拟后端一次 flush 回声,接近一个服务器 tick 内的多个小 PLAY 包;此时 Window 按批计数。输出另含代理 I/O 线程每帧 CPU 时间,以及在 Linux 上读取 `/proc/net/snmp` 得到的全机 TCP 发送段数/帧(含 ACK,其他平台显示 `n/a`)。[2026-10-05 批量 flush 记录](../testing/benchmarks/results/2026-10-05-relay-batched-flush.md)用它比较逐帧 flush 与按读取批次 flush。`-Transport auto|nio|epoll|kqueue`(`--transport`)选择代理使用的网络传输层,默认 `auto`;[2026-10-05 传输层记录](../testing/benchmarks/results/2026-10-05-network-transport.md)比较了 epoll 与 NIO。
|
||||
|
||||
`--websocket 1`(直接运行 Java 时)让代理阶段的客户端通过 `ws://` 连接代理的 WebSocket 监听器:完成升级后,客户端每次 flush 发送一条带掩码的二进制消息,读取时按 RFC 6455 拆出服务器消息的负载。直接阶段仍用 TCP 连接同一个模拟后端,所以代理/直连之比混入了 WebSocket 客户端自身的开销;比较 WebSocket 路径时以代理 I/O 线程每帧 CPU 为准。[2026-10-22 WebSocket 合并记录](../testing/benchmarks/results/2026-10-22-websocket-coalescing.md)用它比较了逐帧消息与按 flush 合并。
|
||||
|
||||
原 stop-and-wait 小样本见 `testing/benchmarks/results/2026-09-25-proxy-session-benchmark-smoke.md`;Window 参数的小样本和 Window=1/16 重复测量见 `testing/benchmarks/results/2026-09-25-proxy-session-window.md`。这些是环回网络上的合成帧对照,不能代表 Forge 整合包、真实后端或跨主机部署的性能。
|
||||
|
||||
## 后端宿主真实服务端验收
|
||||
|
||||
@@ -1,31 +1,91 @@
|
||||
package dev.moonbridge.core.websocket;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.MessageToMessageCodec;
|
||||
import io.netty.channel.ChannelPromise;
|
||||
import io.netty.channel.CoalescingBufferQueue;
|
||||
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
|
||||
|
||||
import java.util.List;
|
||||
import java.nio.channels.ClosedChannelException;
|
||||
|
||||
/**
|
||||
* Carries the raw Minecraft TCP byte stream in binary WebSocket messages. Message boundaries carry no
|
||||
* meaning: the Minecraft frame decoder behind this handler reassembles frames from any chunking, and it
|
||||
* also requests the next read when a message yields no complete frame, so manual-read relays never stall.
|
||||
* Ping, pong and close frames are answered by Netty's protocol handler in front of this one.
|
||||
*
|
||||
* <p>Outbound, the bytes written before a flush become one binary message of at most {@link #MAX_MESSAGE_BYTES}.
|
||||
* The relay writes one Minecraft frame per write and flushes once per read batch, so a burst of small frames costs
|
||||
* one message header instead of one per frame, and a batch of at least 1 KiB is sent by Netty's encoder without
|
||||
* copying. Each write's promise completes when the message carrying it is written.
|
||||
*/
|
||||
public final class WebSocketFrameAdapter extends MessageToMessageCodec<WebSocketFrame, ByteBuf> {
|
||||
@Override protected void decode(ChannelHandlerContext ctx, WebSocketFrame frame, List<Object> out) {
|
||||
if (frame instanceof BinaryWebSocketFrame || frame instanceof ContinuationWebSocketFrame) {
|
||||
if (frame.content().isReadable()) out.add(frame.content().retain());
|
||||
} else if (frame instanceof TextWebSocketFrame) {
|
||||
ctx.close();
|
||||
}
|
||||
public final class WebSocketFrameAdapter extends ChannelDuplexHandler {
|
||||
/** Far below the client mod's 16 MiB frame limit and typical CDN message limits. */
|
||||
static final int MAX_MESSAGE_BYTES = 64 * 1024;
|
||||
|
||||
private CoalescingBufferQueue pending;
|
||||
|
||||
@Override public void handlerAdded(ChannelHandlerContext ctx) {
|
||||
// Counting queued bytes as pending outbound keeps channel writability, and so relay backpressure, accurate.
|
||||
pending = new CoalescingBufferQueue(ctx.channel(), 4, true);
|
||||
}
|
||||
|
||||
@Override protected void encode(ChannelHandlerContext ctx, ByteBuf bytes, List<Object> out) {
|
||||
out.add(new BinaryWebSocketFrame(bytes.retain()));
|
||||
@Override public void handlerRemoved(ChannelHandlerContext ctx) {
|
||||
pending.releaseAndFailAll(ctx, new ClosedChannelException());
|
||||
}
|
||||
|
||||
@Override public void channelRead(ChannelHandlerContext ctx, Object message) {
|
||||
if (!(message instanceof WebSocketFrame frame)) {
|
||||
ctx.fireChannelRead(message);
|
||||
return;
|
||||
}
|
||||
if (frame instanceof BinaryWebSocketFrame || frame instanceof ContinuationWebSocketFrame) {
|
||||
if (frame.content().isReadable()) {
|
||||
ctx.fireChannelRead(frame.content()); // Ownership moves with the payload.
|
||||
return;
|
||||
}
|
||||
} else if (frame instanceof TextWebSocketFrame) {
|
||||
frame.release();
|
||||
ctx.close();
|
||||
return;
|
||||
}
|
||||
frame.release();
|
||||
}
|
||||
|
||||
@Override public void write(ChannelHandlerContext ctx, Object message, ChannelPromise promise) {
|
||||
if (!(message instanceof ByteBuf bytes)) {
|
||||
ctx.write(message, promise);
|
||||
return;
|
||||
}
|
||||
pending.add(bytes, promise);
|
||||
if (pending.readableBytes() >= MAX_MESSAGE_BYTES) writePending(ctx);
|
||||
}
|
||||
|
||||
@Override public void flush(ChannelHandlerContext ctx) {
|
||||
writePending(ctx);
|
||||
ctx.flush();
|
||||
}
|
||||
|
||||
@Override public void close(ChannelHandlerContext ctx, ChannelPromise promise) {
|
||||
// Unflushed writes fail on close, as they would in the channel's own outbound buffer.
|
||||
pending.releaseAndFailAll(ctx, new ClosedChannelException());
|
||||
ctx.close(promise);
|
||||
}
|
||||
|
||||
@Override public void channelInactive(ChannelHandlerContext ctx) {
|
||||
pending.releaseAndFailAll(ctx, new ClosedChannelException());
|
||||
ctx.fireChannelInactive();
|
||||
}
|
||||
|
||||
private void writePending(ChannelHandlerContext ctx) {
|
||||
while (!pending.isEmpty()) {
|
||||
ChannelPromise written = ctx.newPromise();
|
||||
ByteBuf payload = pending.remove(ctx.alloc(), MAX_MESSAGE_BYTES, written);
|
||||
ctx.write(new BinaryWebSocketFrame(payload), written);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package dev.moonbridge.core.websocket;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufUtil;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.embedded.EmbeddedChannel;
|
||||
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
|
||||
@@ -8,10 +9,12 @@ import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
final class WebSocketFrameAdapterTest {
|
||||
@Test
|
||||
@@ -64,4 +67,48 @@ final class WebSocketFrameAdapterTest {
|
||||
frame.release();
|
||||
assertFalse(channel.finish());
|
||||
}
|
||||
|
||||
@Test
|
||||
void writesBeforeAFlushShareOneMessageAndEachPromiseCompletes() {
|
||||
var channel = new EmbeddedChannel(new WebSocketFrameAdapter());
|
||||
var first = channel.write(Unpooled.wrappedBuffer(new byte[]{1, 2}));
|
||||
var second = channel.write(Unpooled.wrappedBuffer(new byte[]{3}));
|
||||
assertNull(channel.readOutbound(), "nothing is sent before the flush");
|
||||
channel.flush();
|
||||
BinaryWebSocketFrame frame = channel.readOutbound();
|
||||
assertArrayEquals(new byte[]{1, 2, 3}, ByteBufUtil.getBytes(frame.content()));
|
||||
assertNull(channel.readOutbound());
|
||||
assertTrue(first.isSuccess());
|
||||
assertTrue(second.isSuccess());
|
||||
frame.release();
|
||||
assertFalse(channel.finish());
|
||||
}
|
||||
|
||||
@Test
|
||||
void largeBatchesAreSplitAtTheMessageLimit() {
|
||||
var channel = new EmbeddedChannel(new WebSocketFrameAdapter());
|
||||
int size = WebSocketFrameAdapter.MAX_MESSAGE_BYTES;
|
||||
channel.write(Unpooled.wrappedBuffer(new byte[size - 10]));
|
||||
channel.write(Unpooled.wrappedBuffer(new byte[30]));
|
||||
channel.flush();
|
||||
BinaryWebSocketFrame full = channel.readOutbound();
|
||||
BinaryWebSocketFrame rest = channel.readOutbound();
|
||||
assertEquals(size, full.content().readableBytes());
|
||||
assertEquals(20, rest.content().readableBytes());
|
||||
full.release();
|
||||
rest.release();
|
||||
assertFalse(channel.finish());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unflushedWritesFailAndAreReleasedOnClose() {
|
||||
var channel = new EmbeddedChannel(new WebSocketFrameAdapter());
|
||||
ByteBuf bytes = Unpooled.buffer().writeBytes(new byte[]{7});
|
||||
var write = channel.write(bytes);
|
||||
channel.close();
|
||||
assertTrue(write.isDone());
|
||||
assertFalse(write.isSuccess());
|
||||
assertEquals(0, bytes.refCnt());
|
||||
assertNull(channel.readOutbound());
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import java.util.concurrent.TimeUnit;
|
||||
public final class ProxySessionBenchmark {
|
||||
private static final int IO_TIMEOUT_MILLIS = 30_000;
|
||||
private static final int SETUP_TIMEOUT_SECONDS = 20;
|
||||
private static final String WEBSOCKET_PATH = "/mc";
|
||||
|
||||
private ProxySessionBenchmark() { }
|
||||
|
||||
@@ -48,6 +49,7 @@ public final class ProxySessionBenchmark {
|
||||
int window = intArg(args, "window", 1);
|
||||
int commandInterceptor = intArg(args, "command-interceptor", 1);
|
||||
int burst = intArg(args, "burst", 1);
|
||||
int webSocket = intArg(args, "websocket", 0);
|
||||
String mode = modeArg(args);
|
||||
var transport = dev.moonbridge.core.session.channel.NetworkTransport.select(
|
||||
dev.moonbridge.core.session.channel.NetworkTransport.Preference.valueOf(
|
||||
@@ -56,7 +58,8 @@ public final class ProxySessionBenchmark {
|
||||
if (connections < 1 || connections > 32 || messages < 1 || messages > 100_000
|
||||
|| warmup < 0 || warmup > 10_000 || payload < 5 || payload > 1_048_576
|
||||
|| repeats < 1 || repeats > 10 || window < 1 || window > 1024
|
||||
|| (commandInterceptor != 0 && commandInterceptor != 1) || burst < 1 || burst > 256) {
|
||||
|| (commandInterceptor != 0 && commandInterceptor != 1) || burst < 1 || burst > 256
|
||||
|| (webSocket != 0 && webSocket != 1)) {
|
||||
throw new IllegalArgumentException("bounds: connections 1..32, messages 1..100000, warmup 0..10000, "
|
||||
+ "payload 5..1048576, repeats 1..10, window 1..1024, burst 1..256");
|
||||
}
|
||||
@@ -83,14 +86,26 @@ public final class ProxySessionBenchmark {
|
||||
proxy.setPlacement(player -> java.util.concurrent.CompletableFuture.completedFuture(
|
||||
Optional.of(PlacementDecision.select("bench"))));
|
||||
if (commandInterceptor == 1) proxy.setCommandDispatcher((player, message, reply, admission) -> false);
|
||||
if (webSocket == 1) {
|
||||
proxy.setWebSocket(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0),
|
||||
new dev.moonbridge.core.websocket.WebSocketFrontend.Settings(WEBSOCKET_PATH, null,
|
||||
dev.moonbridge.core.websocket.ClientAddressPolicy.direct(), Duration.ofSeconds(10)));
|
||||
}
|
||||
try {
|
||||
int proxyPort = ((InetSocketAddress) proxy.start().toCompletableFuture()
|
||||
int tcpPort = ((InetSocketAddress) proxy.start().toCompletableFuture()
|
||||
.get(SETUP_TIMEOUT_SECONDS, TimeUnit.SECONDS).localAddress()).getPort();
|
||||
// Proxy clients use the WebSocket listener when it is enabled; direct clients always use plain TCP,
|
||||
// because the fake backend is a Minecraft TCP server.
|
||||
int proxyPort = webSocket == 1
|
||||
? ((InetSocketAddress) proxy.webSocketChannel().localAddress()).getPort() : tcpPort;
|
||||
System.out.printf("Offline synthetic Minecraft 1.7.10 PLAY echo; mode=%s payload=%d bytes frame=%d bytes "
|
||||
+ "connections=%d measured_roundtrips_per_connection=%d warmup_per_connection=%d "
|
||||
+ "window_per_connection=%d repeats=%d command_interceptor=%d burst=%d transport=%s%n",
|
||||
+ "window_per_connection=%d repeats=%d command_interceptor=%d burst=%d transport=%s "
|
||||
+ "proxy_frontend=%s%n",
|
||||
mode, playPayload.length, playFrame.length, connections, messages, warmup, window, repeats,
|
||||
commandInterceptor, burst, transport.name());
|
||||
commandInterceptor, burst, transport.name(), webSocket == 1 ? "websocket" : "tcp");
|
||||
if (webSocket == 1) System.out.println("Proxy clients connect over plain ws:// and send one masked binary "
|
||||
+ "message per client flush; direct clients use TCP to the same fake backend.");
|
||||
if (burst > 1) System.out.println("Each roundtrip writes " + burst + " frames in one client write; "
|
||||
+ "the fake backend echoes them with one flush. Window counts bursts.");
|
||||
System.out.println("Direct and proxy phases use the same JVM, fake backend, client code, framed payload, "
|
||||
@@ -104,14 +119,14 @@ public final class ProxySessionBenchmark {
|
||||
for (int round = 0; round < repeats; round++) {
|
||||
if (proxyFirst) {
|
||||
runPhase("proxy-" + (round + 1), proxyPort, connections, messages, warmup, window,
|
||||
playFrame, playPayload, proxy, afterTransfer, burst);
|
||||
playFrame, playPayload, proxy, afterTransfer, burst, webSocket == 1);
|
||||
runPhase("direct-" + (round + 1), directPort, connections, messages, warmup, window,
|
||||
playFrame, playPayload, proxy, afterTransfer, burst);
|
||||
playFrame, playPayload, proxy, afterTransfer, burst, webSocket == 1);
|
||||
} else {
|
||||
runPhase("direct-" + (round + 1), directPort, connections, messages, warmup, window,
|
||||
playFrame, playPayload, proxy, afterTransfer, burst);
|
||||
playFrame, playPayload, proxy, afterTransfer, burst, webSocket == 1);
|
||||
runPhase("proxy-" + (round + 1), proxyPort, connections, messages, warmup, window,
|
||||
playFrame, playPayload, proxy, afterTransfer, burst);
|
||||
playFrame, playPayload, proxy, afterTransfer, burst, webSocket == 1);
|
||||
}
|
||||
proxyFirst = !proxyFirst;
|
||||
}
|
||||
@@ -123,14 +138,15 @@ public final class ProxySessionBenchmark {
|
||||
|
||||
private static void runPhase(String name, int port, int connections, int messages, int warmup, int window,
|
||||
byte[] playFrame, byte[] playPayload, ProxySessionListener proxy,
|
||||
boolean afterTransfer, int burst) throws Exception {
|
||||
boolean afterTransfer, int burst, boolean webSocket) throws Exception {
|
||||
List<ClientWorker> clients = new ArrayList<>();
|
||||
CountDownLatch prepared = new CountDownLatch(connections);
|
||||
CountDownLatch begin = new CountDownLatch(1);
|
||||
CountDownLatch finished = new CountDownLatch(connections);
|
||||
for (int i = 0; i < connections; i++) {
|
||||
ClientWorker client = new ClientWorker(name, i, port, messages, warmup, window, playFrame,
|
||||
playPayload, prepared, begin, finished, clients, proxy, afterTransfer, burst);
|
||||
playPayload, prepared, begin, finished, clients, proxy, afterTransfer, burst,
|
||||
webSocket && name.startsWith("proxy"));
|
||||
clients.add(client);
|
||||
}
|
||||
for (ClientWorker client : clients) {
|
||||
@@ -197,6 +213,7 @@ public final class ProxySessionBenchmark {
|
||||
final ProxySessionListener proxy;
|
||||
final boolean afterTransfer;
|
||||
final int burst;
|
||||
final boolean webSocket;
|
||||
volatile Exception failure;
|
||||
volatile Socket socket;
|
||||
long[] latencies;
|
||||
@@ -204,11 +221,12 @@ public final class ProxySessionBenchmark {
|
||||
|
||||
ClientWorker(String phase, int index, int port, int messages, int warmup, int window, byte[] playFrame,
|
||||
byte[] playPayload, CountDownLatch prepared, CountDownLatch begin, CountDownLatch finished,
|
||||
List<ClientWorker> peers, ProxySessionListener proxy, boolean afterTransfer, int burst) {
|
||||
List<ClientWorker> peers, ProxySessionListener proxy, boolean afterTransfer, int burst,
|
||||
boolean webSocket) {
|
||||
this.phase = phase; this.index = index; this.port = port; this.messages = messages;
|
||||
this.warmup = warmup; this.window = window; this.playFrame = playFrame.clone(); this.playPayload = playPayload;
|
||||
this.prepared = prepared; this.begin = begin; this.finished = finished; this.peers = peers;
|
||||
this.proxy = proxy; this.afterTransfer = afterTransfer; this.burst = burst;
|
||||
this.proxy = proxy; this.afterTransfer = afterTransfer; this.burst = burst; this.webSocket = webSocket;
|
||||
}
|
||||
|
||||
@Override public void run() {
|
||||
@@ -217,8 +235,16 @@ public final class ProxySessionBenchmark {
|
||||
socket.setTcpNoDelay(true);
|
||||
socket.setSoTimeout(IO_TIMEOUT_MILLIS);
|
||||
socket.connect(new InetSocketAddress("127.0.0.1", port), 5000);
|
||||
DataInputStream input = new DataInputStream(socket.getInputStream());
|
||||
DataOutputStream output = new DataOutputStream(socket.getOutputStream());
|
||||
DataInputStream input;
|
||||
DataOutputStream output;
|
||||
if (webSocket) {
|
||||
var streams = new WebSocketStreams(socket, port);
|
||||
input = new DataInputStream(streams.input);
|
||||
output = new DataOutputStream(streams.output);
|
||||
} else {
|
||||
input = new DataInputStream(socket.getInputStream());
|
||||
output = new DataOutputStream(socket.getOutputStream());
|
||||
}
|
||||
String username = "B" + (phase.startsWith("proxy") ? "P" : "D") + index + "X";
|
||||
sendHandshakeAndLogin(output, username);
|
||||
readLoginSuccess(input, username);
|
||||
@@ -297,6 +323,107 @@ public final class ProxySessionBenchmark {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A minimal RFC 6455 client over a blocking socket, enough for the benchmark: the output buffers writes and sends
|
||||
* them as one masked binary message per flush, as the client mod sends one message per vanilla write; the input
|
||||
* yields the payload of the server's binary messages as one continuous byte stream.
|
||||
*/
|
||||
private static final class WebSocketStreams {
|
||||
final java.io.OutputStream output;
|
||||
final java.io.InputStream input;
|
||||
|
||||
WebSocketStreams(Socket socket, int port) throws IOException {
|
||||
var raw = new java.io.BufferedOutputStream(socket.getOutputStream(), 64 * 1024);
|
||||
var in = new java.io.BufferedInputStream(socket.getInputStream(), 64 * 1024);
|
||||
byte[] key = new byte[16];
|
||||
new java.security.SecureRandom().nextBytes(key);
|
||||
raw.write(("GET " + WEBSOCKET_PATH + " HTTP/1.1\r\nHost: 127.0.0.1:" + port + "\r\nUpgrade: websocket\r\n"
|
||||
+ "Connection: Upgrade\r\nSec-WebSocket-Key: " + java.util.Base64.getEncoder().encodeToString(key)
|
||||
+ "\r\nSec-WebSocket-Version: 13\r\n\r\n").getBytes(StandardCharsets.US_ASCII));
|
||||
raw.flush();
|
||||
var response = new StringBuilder();
|
||||
while (!response.toString().endsWith("\r\n\r\n")) {
|
||||
int next = in.read();
|
||||
if (next < 0) throw new EOFException("WebSocket upgrade response ended early");
|
||||
response.append((char) next);
|
||||
}
|
||||
if (!response.toString().startsWith("HTTP/1.1 101")) {
|
||||
throw new IOException("WebSocket upgrade refused: " + response.toString().lines().findFirst().orElse(""));
|
||||
}
|
||||
byte[] mask = new byte[4];
|
||||
new java.util.Random(port).nextBytes(mask);
|
||||
output = new java.io.OutputStream() {
|
||||
private final ByteArrayOutputStream pending = new ByteArrayOutputStream();
|
||||
|
||||
@Override public void write(int value) { pending.write(value); }
|
||||
@Override public void write(byte[] bytes, int offset, int length) { pending.write(bytes, offset, length); }
|
||||
|
||||
@Override public void flush() throws IOException {
|
||||
if (pending.size() == 0) return;
|
||||
byte[] payload = pending.toByteArray();
|
||||
pending.reset();
|
||||
raw.write(0x82); // FIN, binary
|
||||
if (payload.length < 126) {
|
||||
raw.write(0x80 | payload.length);
|
||||
} else if (payload.length <= 0xFFFF) {
|
||||
raw.write(0x80 | 126);
|
||||
raw.write(payload.length >>> 8);
|
||||
raw.write(payload.length);
|
||||
} else {
|
||||
raw.write(0x80 | 127);
|
||||
for (int shift = 56; shift >= 0; shift -= 8) raw.write((int) ((long) payload.length >>> shift));
|
||||
}
|
||||
raw.write(mask);
|
||||
for (int i = 0; i < payload.length; i++) payload[i] ^= mask[i & 3];
|
||||
raw.write(payload);
|
||||
raw.flush();
|
||||
}
|
||||
};
|
||||
input = new java.io.InputStream() {
|
||||
private long remaining;
|
||||
|
||||
private boolean nextMessage() throws IOException {
|
||||
while (remaining == 0) {
|
||||
int first = in.read();
|
||||
if (first < 0) return false;
|
||||
int second = in.read();
|
||||
if (second < 0) throw new EOFException("WebSocket frame header ended early");
|
||||
int opcode = first & 0x0F;
|
||||
long length = second & 0x7F;
|
||||
if (length == 126) length = (in.read() << 8) | in.read();
|
||||
else if (length == 127) {
|
||||
length = 0;
|
||||
for (int i = 0; i < 8; i++) length = (length << 8) | in.read();
|
||||
}
|
||||
if ((second & 0x80) != 0) throw new IOException("server frames must not be masked");
|
||||
if (opcode == 0x8) return false;
|
||||
if (opcode == 0x2 || opcode == 0x0) {
|
||||
remaining = length;
|
||||
} else if (in.skip(length) != length) {
|
||||
throw new EOFException("WebSocket control frame ended early");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override public int read() throws IOException {
|
||||
if (!nextMessage()) return -1;
|
||||
int value = in.read();
|
||||
if (value >= 0) remaining--;
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override public int read(byte[] bytes, int offset, int length) throws IOException {
|
||||
if (length == 0) return 0;
|
||||
if (!nextMessage()) return -1;
|
||||
int read = in.read(bytes, offset, (int) Math.min(length, remaining));
|
||||
if (read > 0) remaining -= read;
|
||||
return read;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static PlayerView awaitPlayer(ProxySessionListener proxy, String username) throws Exception {
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(SETUP_TIMEOUT_SECONDS);
|
||||
while (System.nanoTime() < deadline) {
|
||||
@@ -536,7 +663,7 @@ public final class ProxySessionBenchmark {
|
||||
int found = fallback;
|
||||
boolean seen = false;
|
||||
List<String> known = List.of("connections", "messages", "warmup", "payload", "repeats", "window",
|
||||
"mode", "command-interceptor", "burst", "transport");
|
||||
"mode", "command-interceptor", "burst", "transport", "websocket");
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if (args[i].startsWith("--") && !known.contains(args[i].substring(2)))
|
||||
throw new IllegalArgumentException("unknown option " + args[i]);
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
burst32 base pair1 direct-1: roundtrips=24000 echoes=24000 correct=24000 errors=0 roundtrips/s=12583.7 warmup_roundtrips=2400 window=4 one_way_MiB/s=12.29 latency_ms_p50=1.261 p95=1.433 p99=1.583 elapsed_ms=1907.228 frames/s=402678.6 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
burst32 base pair1 proxy-1: roundtrips=24000 echoes=24000 correct=24000 errors=0 roundtrips/s=22785.8 warmup_roundtrips=2400 window=4 one_way_MiB/s=22.25 latency_ms_p50=0.684 p95=0.988 p99=1.356 elapsed_ms=1053.286 frames/s=729146.8 proxy_io_cpu_ns/frame=2931 tcp_out_segments/frame=n/a
|
||||
burst32 base pair1 proxy-2: roundtrips=24000 echoes=24000 correct=24000 errors=0 roundtrips/s=21769.5 warmup_roundtrips=2400 window=4 one_way_MiB/s=21.26 latency_ms_p50=0.717 p95=0.971 p99=1.037 elapsed_ms=1102.461 frames/s=696623.1 proxy_io_cpu_ns/frame=3262 tcp_out_segments/frame=n/a
|
||||
burst32 base pair1 direct-2: roundtrips=24000 echoes=24000 correct=24000 errors=0 roundtrips/s=12603.3 warmup_roundtrips=2400 window=4 one_way_MiB/s=12.31 latency_ms_p50=1.257 p95=1.410 p99=1.497 elapsed_ms=1904.268 frames/s=403304.5 proxy_io_cpu_ns/frame=1 tcp_out_segments/frame=n/a
|
||||
burst32 candidate pair1 direct-1: roundtrips=24000 echoes=24000 correct=24000 errors=0 roundtrips/s=12815.3 warmup_roundtrips=2400 window=4 one_way_MiB/s=12.51 latency_ms_p50=1.244 p95=1.370 p99=1.447 elapsed_ms=1872.758 frames/s=410090.3 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
burst32 candidate pair1 proxy-1: roundtrips=24000 echoes=24000 correct=24000 errors=0 roundtrips/s=23474.5 warmup_roundtrips=2400 window=4 one_way_MiB/s=22.92 latency_ms_p50=0.663 p95=0.975 p99=1.082 elapsed_ms=1022.387 frames/s=751183.6 proxy_io_cpu_ns/frame=2461 tcp_out_segments/frame=n/a
|
||||
burst32 candidate pair1 proxy-2: roundtrips=24000 echoes=24000 correct=24000 errors=0 roundtrips/s=22829.4 warmup_roundtrips=2400 window=4 one_way_MiB/s=22.29 latency_ms_p50=0.666 p95=0.995 p99=1.072 elapsed_ms=1051.277 frames/s=730540.3 proxy_io_cpu_ns/frame=2672 tcp_out_segments/frame=n/a
|
||||
burst32 candidate pair1 direct-2: roundtrips=24000 echoes=24000 correct=24000 errors=0 roundtrips/s=12863.1 warmup_roundtrips=2400 window=4 one_way_MiB/s=12.56 latency_ms_p50=1.246 p95=1.355 p99=1.404 elapsed_ms=1865.809 frames/s=411617.8 proxy_io_cpu_ns/frame=1 tcp_out_segments/frame=n/a
|
||||
burst32 candidate pair2 direct-1: roundtrips=24000 echoes=24000 correct=24000 errors=0 roundtrips/s=12597.6 warmup_roundtrips=2400 window=4 one_way_MiB/s=12.30 latency_ms_p50=1.263 p95=1.393 p99=1.455 elapsed_ms=1905.124 frames/s=403123.4 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
burst32 candidate pair2 proxy-1: roundtrips=24000 echoes=24000 correct=24000 errors=0 roundtrips/s=23226.8 warmup_roundtrips=2400 window=4 one_way_MiB/s=22.68 latency_ms_p50=0.661 p95=1.021 p99=1.495 elapsed_ms=1033.291 frames/s=743256.6 proxy_io_cpu_ns/frame=2476 tcp_out_segments/frame=n/a
|
||||
burst32 candidate pair2 proxy-2: roundtrips=24000 echoes=24000 correct=24000 errors=0 roundtrips/s=22287.7 warmup_roundtrips=2400 window=4 one_way_MiB/s=21.77 latency_ms_p50=0.681 p95=0.995 p99=1.060 elapsed_ms=1076.828 frames/s=713205.8 proxy_io_cpu_ns/frame=2805 tcp_out_segments/frame=n/a
|
||||
burst32 candidate pair2 direct-2: roundtrips=24000 echoes=24000 correct=24000 errors=0 roundtrips/s=12749.8 warmup_roundtrips=2400 window=4 one_way_MiB/s=12.45 latency_ms_p50=1.253 p95=1.374 p99=1.425 elapsed_ms=1882.384 frames/s=407993.2 proxy_io_cpu_ns/frame=1 tcp_out_segments/frame=n/a
|
||||
burst32 base pair2 direct-1: roundtrips=24000 echoes=24000 correct=24000 errors=0 roundtrips/s=12927.6 warmup_roundtrips=2400 window=4 one_way_MiB/s=12.62 latency_ms_p50=1.232 p95=1.357 p99=1.420 elapsed_ms=1856.498 frames/s=413682.0 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
burst32 base pair2 proxy-1: roundtrips=24000 echoes=24000 correct=24000 errors=0 roundtrips/s=23085.9 warmup_roundtrips=2400 window=4 one_way_MiB/s=22.54 latency_ms_p50=0.676 p95=0.960 p99=1.141 elapsed_ms=1039.595 frames/s=738749.2 proxy_io_cpu_ns/frame=3061 tcp_out_segments/frame=n/a
|
||||
burst32 base pair2 proxy-2: roundtrips=24000 echoes=24000 correct=24000 errors=0 roundtrips/s=22695.6 warmup_roundtrips=2400 window=4 one_way_MiB/s=22.16 latency_ms_p50=0.678 p95=0.944 p99=1.016 elapsed_ms=1057.472 frames/s=726260.7 proxy_io_cpu_ns/frame=3246 tcp_out_segments/frame=n/a
|
||||
burst32 base pair2 direct-2: roundtrips=24000 echoes=24000 correct=24000 errors=0 roundtrips/s=12926.4 warmup_roundtrips=2400 window=4 one_way_MiB/s=12.62 latency_ms_p50=1.232 p95=1.354 p99=1.405 elapsed_ms=1856.660 frames/s=413646.1 proxy_io_cpu_ns/frame=1 tcp_out_segments/frame=n/a
|
||||
burst32 base pair3 direct-1: roundtrips=24000 echoes=24000 correct=24000 errors=0 roundtrips/s=12705.6 warmup_roundtrips=2400 window=4 one_way_MiB/s=12.41 latency_ms_p50=1.256 p95=1.377 p99=1.438 elapsed_ms=1888.931 frames/s=406579.1 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
burst32 base pair3 proxy-1: roundtrips=24000 echoes=24000 correct=24000 errors=0 roundtrips/s=21891.5 warmup_roundtrips=2400 window=4 one_way_MiB/s=21.38 latency_ms_p50=0.704 p95=0.971 p99=1.043 elapsed_ms=1096.316 frames/s=700527.9 proxy_io_cpu_ns/frame=3233 tcp_out_segments/frame=n/a
|
||||
burst32 base pair3 proxy-2: roundtrips=24000 echoes=24000 correct=24000 errors=0 roundtrips/s=21812.2 warmup_roundtrips=2400 window=4 one_way_MiB/s=21.30 latency_ms_p50=0.700 p95=0.966 p99=1.020 elapsed_ms=1100.299 frames/s=697991.9 proxy_io_cpu_ns/frame=3360 tcp_out_segments/frame=n/a
|
||||
burst32 base pair3 direct-2: roundtrips=24000 echoes=24000 correct=24000 errors=0 roundtrips/s=12653.3 warmup_roundtrips=2400 window=4 one_way_MiB/s=12.36 latency_ms_p50=1.259 p95=1.380 p99=1.434 elapsed_ms=1896.745 frames/s=404904.2 proxy_io_cpu_ns/frame=1 tcp_out_segments/frame=n/a
|
||||
burst32 candidate pair3 direct-1: roundtrips=24000 echoes=24000 correct=24000 errors=0 roundtrips/s=12817.3 warmup_roundtrips=2400 window=4 one_way_MiB/s=12.52 latency_ms_p50=1.242 p95=1.375 p99=1.438 elapsed_ms=1872.470 frames/s=410153.4 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
burst32 candidate pair3 proxy-1: roundtrips=24000 echoes=24000 correct=24000 errors=0 roundtrips/s=23092.3 warmup_roundtrips=2400 window=4 one_way_MiB/s=22.55 latency_ms_p50=0.679 p95=0.974 p99=1.181 elapsed_ms=1039.308 frames/s=738952.9 proxy_io_cpu_ns/frame=2245 tcp_out_segments/frame=n/a
|
||||
burst32 candidate pair3 proxy-2: roundtrips=24000 echoes=24000 correct=24000 errors=0 roundtrips/s=22549.1 warmup_roundtrips=2400 window=4 one_way_MiB/s=22.02 latency_ms_p50=0.686 p95=0.984 p99=1.061 elapsed_ms=1064.346 frames/s=721569.9 proxy_io_cpu_ns/frame=2625 tcp_out_segments/frame=n/a
|
||||
burst32 candidate pair3 direct-2: roundtrips=24000 echoes=24000 correct=24000 errors=0 roundtrips/s=12815.6 warmup_roundtrips=2400 window=4 one_way_MiB/s=12.52 latency_ms_p50=1.240 p95=1.368 p99=1.423 elapsed_ms=1872.718 frames/s=410099.1 proxy_io_cpu_ns/frame=1 tcp_out_segments/frame=n/a
|
||||
w1 base pair1 direct-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=105640.9 warmup_roundtrips=2000 window=1 one_way_MiB/s=103.16 latency_ms_p50=0.031 p95=0.067 p99=0.083 elapsed_ms=189.321 frames/s=105640.9 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w1 base pair1 proxy-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=45248.7 warmup_roundtrips=2000 window=1 one_way_MiB/s=44.19 latency_ms_p50=0.087 p95=0.139 p99=0.167 elapsed_ms=442.002 frames/s=45248.7 proxy_io_cpu_ns/frame=42238 tcp_out_segments/frame=n/a
|
||||
w1 base pair1 proxy-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=52980.9 warmup_roundtrips=2000 window=1 one_way_MiB/s=51.74 latency_ms_p50=0.068 p95=0.123 p99=0.147 elapsed_ms=377.495 frames/s=52980.9 proxy_io_cpu_ns/frame=32876 tcp_out_segments/frame=n/a
|
||||
w1 base pair1 direct-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=114107.2 warmup_roundtrips=2000 window=1 one_way_MiB/s=111.43 latency_ms_p50=0.030 p95=0.063 p99=0.081 elapsed_ms=175.274 frames/s=114107.2 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w1 candidate pair1 direct-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=105230.5 warmup_roundtrips=2000 window=1 one_way_MiB/s=102.76 latency_ms_p50=0.032 p95=0.067 p99=0.082 elapsed_ms=190.059 frames/s=105230.5 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w1 candidate pair1 proxy-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=45871.8 warmup_roundtrips=2000 window=1 one_way_MiB/s=44.80 latency_ms_p50=0.086 p95=0.136 p99=0.161 elapsed_ms=435.997 frames/s=45871.8 proxy_io_cpu_ns/frame=41337 tcp_out_segments/frame=n/a
|
||||
w1 candidate pair1 proxy-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=50052.6 warmup_roundtrips=2000 window=1 one_way_MiB/s=48.88 latency_ms_p50=0.074 p95=0.127 p99=0.148 elapsed_ms=399.580 frames/s=50052.6 proxy_io_cpu_ns/frame=34907 tcp_out_segments/frame=n/a
|
||||
w1 candidate pair1 direct-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=108586.7 warmup_roundtrips=2000 window=1 one_way_MiB/s=106.04 latency_ms_p50=0.032 p95=0.066 p99=0.082 elapsed_ms=184.185 frames/s=108586.7 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w1 candidate pair2 direct-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=104119.6 warmup_roundtrips=2000 window=1 one_way_MiB/s=101.68 latency_ms_p50=0.032 p95=0.067 p99=0.083 elapsed_ms=192.087 frames/s=104119.6 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w1 candidate pair2 proxy-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=44841.2 warmup_roundtrips=2000 window=1 one_way_MiB/s=43.79 latency_ms_p50=0.086 p95=0.144 p99=0.172 elapsed_ms=446.018 frames/s=44841.2 proxy_io_cpu_ns/frame=42779 tcp_out_segments/frame=n/a
|
||||
w1 candidate pair2 proxy-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=54747.6 warmup_roundtrips=2000 window=1 one_way_MiB/s=53.46 latency_ms_p50=0.066 p95=0.119 p99=0.139 elapsed_ms=365.313 frames/s=54747.6 proxy_io_cpu_ns/frame=31030 tcp_out_segments/frame=n/a
|
||||
w1 candidate pair2 direct-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=109218.5 warmup_roundtrips=2000 window=1 one_way_MiB/s=106.66 latency_ms_p50=0.031 p95=0.066 p99=0.083 elapsed_ms=183.119 frames/s=109218.5 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w1 base pair2 direct-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=104474.6 warmup_roundtrips=2000 window=1 one_way_MiB/s=102.03 latency_ms_p50=0.032 p95=0.066 p99=0.083 elapsed_ms=191.434 frames/s=104474.6 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w1 base pair2 proxy-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=47653.8 warmup_roundtrips=2000 window=1 one_way_MiB/s=46.54 latency_ms_p50=0.081 p95=0.132 p99=0.157 elapsed_ms=419.694 frames/s=47653.8 proxy_io_cpu_ns/frame=38457 tcp_out_segments/frame=n/a
|
||||
w1 base pair2 proxy-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=53074.9 warmup_roundtrips=2000 window=1 one_way_MiB/s=51.83 latency_ms_p50=0.067 p95=0.121 p99=0.142 elapsed_ms=376.826 frames/s=53074.9 proxy_io_cpu_ns/frame=32565 tcp_out_segments/frame=n/a
|
||||
w1 base pair2 direct-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=114002.4 warmup_roundtrips=2000 window=1 one_way_MiB/s=111.33 latency_ms_p50=0.030 p95=0.063 p99=0.080 elapsed_ms=175.435 frames/s=114002.4 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w1 base pair3 direct-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=106822.1 warmup_roundtrips=2000 window=1 one_way_MiB/s=104.32 latency_ms_p50=0.031 p95=0.066 p99=0.081 elapsed_ms=187.227 frames/s=106822.1 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w1 base pair3 proxy-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=47156.3 warmup_roundtrips=2000 window=1 one_way_MiB/s=46.05 latency_ms_p50=0.081 p95=0.134 p99=0.159 elapsed_ms=424.122 frames/s=47156.3 proxy_io_cpu_ns/frame=39386 tcp_out_segments/frame=n/a
|
||||
w1 base pair3 proxy-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=51705.7 warmup_roundtrips=2000 window=1 one_way_MiB/s=50.49 latency_ms_p50=0.069 p95=0.128 p99=0.148 elapsed_ms=386.804 frames/s=51705.7 proxy_io_cpu_ns/frame=33936 tcp_out_segments/frame=n/a
|
||||
w1 base pair3 direct-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=110260.9 warmup_roundtrips=2000 window=1 one_way_MiB/s=107.68 latency_ms_p50=0.031 p95=0.065 p99=0.081 elapsed_ms=181.388 frames/s=110260.9 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w1 candidate pair3 direct-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=103770.6 warmup_roundtrips=2000 window=1 one_way_MiB/s=101.34 latency_ms_p50=0.032 p95=0.067 p99=0.084 elapsed_ms=192.733 frames/s=103770.6 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w1 candidate pair3 proxy-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=48135.9 warmup_roundtrips=2000 window=1 one_way_MiB/s=47.01 latency_ms_p50=0.077 p95=0.135 p99=0.159 elapsed_ms=415.491 frames/s=48135.9 proxy_io_cpu_ns/frame=38654 tcp_out_segments/frame=n/a
|
||||
w1 candidate pair3 proxy-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=52548.8 warmup_roundtrips=2000 window=1 one_way_MiB/s=51.32 latency_ms_p50=0.070 p95=0.119 p99=0.139 elapsed_ms=380.599 frames/s=52548.8 proxy_io_cpu_ns/frame=32329 tcp_out_segments/frame=n/a
|
||||
w1 candidate pair3 direct-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=108087.4 warmup_roundtrips=2000 window=1 one_way_MiB/s=105.55 latency_ms_p50=0.032 p95=0.066 p99=0.082 elapsed_ms=185.035 frames/s=108087.4 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w16 base pair1 direct-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=141243.0 warmup_roundtrips=2000 window=16 one_way_MiB/s=137.93 latency_ms_p50=0.432 p95=0.618 p99=0.829 elapsed_ms=141.600 frames/s=141243.0 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w16 base pair1 proxy-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=39253.1 warmup_roundtrips=2000 window=16 one_way_MiB/s=38.33 latency_ms_p50=1.372 p95=2.361 p99=6.845 elapsed_ms=509.514 frames/s=39253.1 proxy_io_cpu_ns/frame=38786 tcp_out_segments/frame=n/a
|
||||
w16 base pair1 proxy-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=43469.8 warmup_roundtrips=2000 window=16 one_way_MiB/s=42.45 latency_ms_p50=1.408 p95=2.244 p99=2.741 elapsed_ms=460.089 frames/s=43469.8 proxy_io_cpu_ns/frame=38476 tcp_out_segments/frame=n/a
|
||||
w16 base pair1 direct-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=123735.9 warmup_roundtrips=2000 window=16 one_way_MiB/s=120.84 latency_ms_p50=0.515 p95=0.723 p99=0.790 elapsed_ms=161.635 frames/s=123735.9 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w16 candidate pair1 direct-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=141490.9 warmup_roundtrips=2000 window=16 one_way_MiB/s=138.17 latency_ms_p50=0.432 p95=0.606 p99=0.753 elapsed_ms=141.352 frames/s=141490.9 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w16 candidate pair1 proxy-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=43007.0 warmup_roundtrips=2000 window=16 one_way_MiB/s=42.00 latency_ms_p50=1.377 p95=2.283 p99=3.467 elapsed_ms=465.041 frames/s=43007.0 proxy_io_cpu_ns/frame=37807 tcp_out_segments/frame=n/a
|
||||
w16 candidate pair1 proxy-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=75601.9 warmup_roundtrips=2000 window=16 one_way_MiB/s=73.83 latency_ms_p50=0.578 p95=1.949 p99=2.392 elapsed_ms=264.544 frames/s=75601.9 proxy_io_cpu_ns/frame=23847 tcp_out_segments/frame=n/a
|
||||
w16 candidate pair1 direct-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=135244.0 warmup_roundtrips=2000 window=16 one_way_MiB/s=132.07 latency_ms_p50=0.461 p95=0.604 p99=0.646 elapsed_ms=147.881 frames/s=135244.0 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w16 candidate pair2 direct-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=137665.9 warmup_roundtrips=2000 window=16 one_way_MiB/s=134.44 latency_ms_p50=0.461 p95=0.614 p99=0.747 elapsed_ms=145.279 frames/s=137665.9 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w16 candidate pair2 proxy-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=39080.3 warmup_roundtrips=2000 window=16 one_way_MiB/s=38.16 latency_ms_p50=1.500 p95=2.746 p99=3.944 elapsed_ms=511.767 frames/s=39080.3 proxy_io_cpu_ns/frame=47882 tcp_out_segments/frame=n/a
|
||||
w16 candidate pair2 proxy-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=51023.0 warmup_roundtrips=2000 window=16 one_way_MiB/s=49.83 latency_ms_p50=1.248 p95=2.412 p99=2.700 elapsed_ms=391.980 frames/s=51023.0 proxy_io_cpu_ns/frame=32012 tcp_out_segments/frame=n/a
|
||||
w16 candidate pair2 direct-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=133238.9 warmup_roundtrips=2000 window=16 one_way_MiB/s=130.12 latency_ms_p50=0.488 p95=0.598 p99=0.644 elapsed_ms=150.106 frames/s=133238.9 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w16 base pair2 direct-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=141438.8 warmup_roundtrips=2000 window=16 one_way_MiB/s=138.12 latency_ms_p50=0.449 p95=0.594 p99=0.685 elapsed_ms=141.404 frames/s=141438.8 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w16 base pair2 proxy-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=86781.7 warmup_roundtrips=2000 window=16 one_way_MiB/s=84.75 latency_ms_p50=0.591 p95=1.275 p99=1.927 elapsed_ms=230.463 frames/s=86781.7 proxy_io_cpu_ns/frame=36315 tcp_out_segments/frame=n/a
|
||||
w16 base pair2 proxy-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=116830.8 warmup_roundtrips=2000 window=16 one_way_MiB/s=114.09 latency_ms_p50=0.507 p95=0.822 p99=1.039 elapsed_ms=171.188 frames/s=116830.8 proxy_io_cpu_ns/frame=28301 tcp_out_segments/frame=n/a
|
||||
w16 base pair2 direct-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=138188.3 warmup_roundtrips=2000 window=16 one_way_MiB/s=134.95 latency_ms_p50=0.460 p95=0.693 p99=0.763 elapsed_ms=144.730 frames/s=138188.3 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w16 base pair3 direct-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=141466.3 warmup_roundtrips=2000 window=16 one_way_MiB/s=138.15 latency_ms_p50=0.447 p95=0.607 p99=0.758 elapsed_ms=141.376 frames/s=141466.3 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w16 base pair3 proxy-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=98001.2 warmup_roundtrips=2000 window=16 one_way_MiB/s=95.70 latency_ms_p50=0.583 p95=1.199 p99=1.626 elapsed_ms=204.079 frames/s=98001.2 proxy_io_cpu_ns/frame=33560 tcp_out_segments/frame=n/a
|
||||
w16 base pair3 proxy-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=119096.2 warmup_roundtrips=2000 window=16 one_way_MiB/s=116.30 latency_ms_p50=0.447 p95=0.761 p99=1.922 elapsed_ms=167.931 frames/s=119096.2 proxy_io_cpu_ns/frame=25992 tcp_out_segments/frame=n/a
|
||||
w16 base pair3 direct-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=141182.4 warmup_roundtrips=2000 window=16 one_way_MiB/s=137.87 latency_ms_p50=0.451 p95=0.622 p99=0.730 elapsed_ms=141.661 frames/s=141182.4 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w16 candidate pair3 direct-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=130886.4 warmup_roundtrips=2000 window=16 one_way_MiB/s=127.82 latency_ms_p50=0.472 p95=0.702 p99=0.765 elapsed_ms=152.804 frames/s=130886.4 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w16 candidate pair3 proxy-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=92458.8 warmup_roundtrips=2000 window=16 one_way_MiB/s=90.29 latency_ms_p50=0.643 p95=1.117 p99=1.449 elapsed_ms=216.313 frames/s=92458.8 proxy_io_cpu_ns/frame=33158 tcp_out_segments/frame=n/a
|
||||
w16 candidate pair3 proxy-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=135701.5 warmup_roundtrips=2000 window=16 one_way_MiB/s=132.52 latency_ms_p50=0.398 p95=0.892 p99=1.643 elapsed_ms=147.382 frames/s=135701.5 proxy_io_cpu_ns/frame=22885 tcp_out_segments/frame=n/a
|
||||
w16 candidate pair3 direct-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=133108.5 warmup_roundtrips=2000 window=16 one_way_MiB/s=129.99 latency_ms_p50=0.493 p95=0.685 p99=0.767 elapsed_ms=150.253 frames/s=133108.5 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w16 candidate pair4 direct-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=140845.7 warmup_roundtrips=2000 window=16 one_way_MiB/s=137.54 latency_ms_p50=0.432 p95=0.632 p99=0.922 elapsed_ms=141.999 frames/s=140845.7 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w16 candidate pair4 proxy-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=105507.4 warmup_roundtrips=2000 window=16 one_way_MiB/s=103.03 latency_ms_p50=0.493 p95=1.142 p99=1.618 elapsed_ms=189.560 frames/s=105507.4 proxy_io_cpu_ns/frame=29733 tcp_out_segments/frame=n/a
|
||||
w16 candidate pair4 proxy-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=113502.5 warmup_roundtrips=2000 window=16 one_way_MiB/s=110.84 latency_ms_p50=0.533 p95=0.882 p99=1.085 elapsed_ms=176.208 frames/s=113502.5 proxy_io_cpu_ns/frame=23853 tcp_out_segments/frame=n/a
|
||||
w16 candidate pair4 direct-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=129767.7 warmup_roundtrips=2000 window=16 one_way_MiB/s=126.73 latency_ms_p50=0.493 p95=0.702 p99=0.768 elapsed_ms=154.122 frames/s=129767.7 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w16 base pair4 direct-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=143172.5 warmup_roundtrips=2000 window=16 one_way_MiB/s=139.82 latency_ms_p50=0.442 p95=0.588 p99=0.779 elapsed_ms=139.692 frames/s=143172.5 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w16 base pair4 proxy-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=35262.9 warmup_roundtrips=2000 window=16 one_way_MiB/s=34.44 latency_ms_p50=1.880 p95=3.391 p99=3.917 elapsed_ms=567.168 frames/s=35262.9 proxy_io_cpu_ns/frame=35953 tcp_out_segments/frame=n/a
|
||||
w16 base pair4 proxy-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=125974.1 warmup_roundtrips=2000 window=16 one_way_MiB/s=123.02 latency_ms_p50=0.472 p95=0.778 p99=1.067 elapsed_ms=158.763 frames/s=125974.1 proxy_io_cpu_ns/frame=23298 tcp_out_segments/frame=n/a
|
||||
w16 base pair4 direct-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=147270.1 warmup_roundtrips=2000 window=16 one_way_MiB/s=143.82 latency_ms_p50=0.408 p95=0.623 p99=0.944 elapsed_ms=135.805 frames/s=147270.1 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w16 base pair5 direct-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=140729.0 warmup_roundtrips=2000 window=16 one_way_MiB/s=137.43 latency_ms_p50=0.448 p95=0.603 p99=0.722 elapsed_ms=142.117 frames/s=140729.0 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w16 base pair5 proxy-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=45200.4 warmup_roundtrips=2000 window=16 one_way_MiB/s=44.14 latency_ms_p50=1.315 p95=2.339 p99=3.270 elapsed_ms=442.474 frames/s=45200.4 proxy_io_cpu_ns/frame=45631 tcp_out_segments/frame=n/a
|
||||
w16 base pair5 proxy-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=46697.0 warmup_roundtrips=2000 window=16 one_way_MiB/s=45.60 latency_ms_p50=1.438 p95=2.160 p99=2.605 elapsed_ms=428.293 frames/s=46697.0 proxy_io_cpu_ns/frame=31603 tcp_out_segments/frame=n/a
|
||||
w16 base pair5 direct-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=138173.3 warmup_roundtrips=2000 window=16 one_way_MiB/s=134.93 latency_ms_p50=0.460 p95=0.582 p99=0.623 elapsed_ms=144.746 frames/s=138173.3 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w16 candidate pair5 direct-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=141639.5 warmup_roundtrips=2000 window=16 one_way_MiB/s=138.32 latency_ms_p50=0.443 p95=0.653 p99=0.813 elapsed_ms=141.204 frames/s=141639.5 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w16 candidate pair5 proxy-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=107445.1 warmup_roundtrips=2000 window=16 one_way_MiB/s=104.93 latency_ms_p50=0.504 p95=1.134 p99=1.536 elapsed_ms=186.142 frames/s=107445.1 proxy_io_cpu_ns/frame=27747 tcp_out_segments/frame=n/a
|
||||
w16 candidate pair5 proxy-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=116627.4 warmup_roundtrips=2000 window=16 one_way_MiB/s=113.89 latency_ms_p50=0.477 p95=0.869 p99=1.395 elapsed_ms=171.486 frames/s=116627.4 proxy_io_cpu_ns/frame=22746 tcp_out_segments/frame=n/a
|
||||
w16 candidate pair5 direct-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=131753.2 warmup_roundtrips=2000 window=16 one_way_MiB/s=128.67 latency_ms_p50=0.390 p95=0.829 p99=2.128 elapsed_ms=151.799 frames/s=131753.2 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w16 candidate pair6 direct-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=144295.0 warmup_roundtrips=2000 window=16 one_way_MiB/s=140.91 latency_ms_p50=0.436 p95=0.626 p99=0.868 elapsed_ms=138.605 frames/s=144295.0 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w16 candidate pair6 proxy-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=88704.4 warmup_roundtrips=2000 window=16 one_way_MiB/s=86.63 latency_ms_p50=0.567 p95=1.376 p99=3.224 elapsed_ms=225.468 frames/s=88704.4 proxy_io_cpu_ns/frame=32842 tcp_out_segments/frame=n/a
|
||||
w16 candidate pair6 proxy-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=107369.8 warmup_roundtrips=2000 window=16 one_way_MiB/s=104.85 latency_ms_p50=0.484 p95=1.057 p99=2.170 elapsed_ms=186.272 frames/s=107369.8 proxy_io_cpu_ns/frame=27910 tcp_out_segments/frame=n/a
|
||||
w16 candidate pair6 direct-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=142384.9 warmup_roundtrips=2000 window=16 one_way_MiB/s=139.05 latency_ms_p50=0.366 p95=0.770 p99=2.244 elapsed_ms=140.464 frames/s=142384.9 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w16 base pair6 direct-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=132071.5 warmup_roundtrips=2000 window=16 one_way_MiB/s=128.98 latency_ms_p50=0.463 p95=0.723 p99=1.400 elapsed_ms=151.433 frames/s=132071.5 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
w16 base pair6 proxy-1: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=94409.4 warmup_roundtrips=2000 window=16 one_way_MiB/s=92.20 latency_ms_p50=0.625 p95=1.107 p99=1.378 elapsed_ms=211.843 frames/s=94409.4 proxy_io_cpu_ns/frame=35725 tcp_out_segments/frame=n/a
|
||||
w16 base pair6 proxy-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=110427.3 warmup_roundtrips=2000 window=16 one_way_MiB/s=107.84 latency_ms_p50=0.453 p95=0.948 p99=1.988 elapsed_ms=181.115 frames/s=110427.3 proxy_io_cpu_ns/frame=19957 tcp_out_segments/frame=n/a
|
||||
w16 base pair6 direct-2: roundtrips=20000 echoes=20000 correct=20000 errors=0 roundtrips/s=129954.9 warmup_roundtrips=2000 window=16 one_way_MiB/s=126.91 latency_ms_p50=0.497 p95=0.659 p99=0.740 elapsed_ms=153.900 frames/s=129954.9 proxy_io_cpu_ns/frame=0 tcp_out_segments/frame=n/a
|
||||
@@ -0,0 +1,56 @@
|
||||
# WebSocket: one binary message per flush instead of per frame — 2026-10-22
|
||||
|
||||
Base revision: `0253130` (main). Candidate: `WebSocketFrameAdapter` gathers every `ByteBuf` written before a flush in a Netty `CoalescingBufferQueue` and writes them as one `BinaryWebSocketFrame` of at most 64 KiB. Before, each Minecraft frame the relay wrote became its own binary message. Netty's `WebSocket08FrameEncoder` allocates a new buffer and copies header and payload for every message under 1 KiB (`GATHERING_WRITE_THRESHOLD = 1024`), and most PLAY packets are smaller than that. The relay already flushes once per read batch, so the change adds no waiting; each write's promise completes when the message carrying it is written. Inbound handling is unchanged. The plain TCP path does not pass through this handler.
|
||||
|
||||
**Outcome: adopted.**
|
||||
|
||||
## Decision rule (set before the comparison)
|
||||
|
||||
Adopt only if, over WebSocket, the burst32 median proxy I/O CPU per frame is at least 10% below the base and burst32 proxy frames/s is not lower. Window 1 and window 16 must show no consistent regression: CPU and p50 latency medians within +10%, or overlapping ranges.
|
||||
|
||||
## Benchmark mode added for this
|
||||
|
||||
`ProxySessionBenchmark --websocket 1` enables the proxy's WebSocket listener (plain `ws://`, path `/mc`, direct address policy). Proxy-phase clients upgrade over HTTP and then send one masked binary message per client flush, reading the server's messages back as one byte stream. Direct-phase clients still use TCP to the same fake backend, because that backend is a Minecraft TCP server. The proxy/direct ratio therefore includes the client's own WebSocket cost; proxy I/O CPU per frame is the metric for the proxy's WebSocket path.
|
||||
|
||||
## Environment and method
|
||||
|
||||
Apple M4 (10 cores), macOS, Zulu OpenJDK 25.0.4.1, Netty 4.2.18.Final, kqueue transport, loopback only. Both variants were built with `:proxy-core:installDist`, and the same benchmark source was compiled against each `moonbridge.jar`. For each workload, three base/candidate JVM pairs ran, alternating which variant started (base first in pairs 1 and 3). Each JVM ran `--repeats 2`. Window 16 was noisy in both variants, so three more pairs ran for it (candidate first in pairs 4 and 6), giving 12 phases per variant. Every echo was checked; all 96 phases report `errors=0`.
|
||||
|
||||
```sh
|
||||
java -Xms512m -Xmx512m --enable-native-access=ALL-UNNAMED -cp "<variant>/classes:<variant>/moonbridge.jar" \
|
||||
dev.moonbridge.core.session.ProxySessionBenchmark --transport kqueue --websocket 1 \
|
||||
--connections 4 --messages 6000 --warmup 600 --payload 32 --window 4 --burst 32 --repeats 2 # burst32
|
||||
--connections 4 --messages 5000 --warmup 500 --payload 1024 --window 1 --burst 1 --repeats 2 # w1
|
||||
--connections 4 --messages 5000 --warmup 500 --payload 1024 --window 16 --burst 1 --repeats 2 # w16
|
||||
```
|
||||
|
||||
## Results
|
||||
|
||||
Values are the median and [min–max] of the proxy phases (6 per cell; 12 for w16).
|
||||
|
||||
| Workload | Variant | Proxy frames/s | p50 ms | p95 ms | Proxy I/O CPU ns/frame |
|
||||
| --- | --- | ---: | ---: | ---: | ---: |
|
||||
| burst32 | base | 713394 [696623–738749] | 0.692 [0.676–0.717] | 0.968 [0.944–0.988] | 3240 [2931–3360] |
|
||||
| burst32 | candidate | 734747 [713206–751184] | 0.673 [0.661–0.686] | 0.990 [0.974–1.021] | 2550 [2245–2805] |
|
||||
| w1 | base | 49680 [45249–53075] | 0.075 [0.067–0.087] | 0.130 [0.121–0.139] | 36196 [32565–42238] |
|
||||
| w1 | candidate | 49094 [44841–54748] | 0.075 [0.066–0.086] | 0.131 [0.119–0.144] | 36780 [31030–42779] |
|
||||
| w16 (pairs 1–3) | base | 92391 [39253–119096] | 0.587 [0.447–1.408] | 1.237 [0.761–2.361] | 34938 [25992–38786] |
|
||||
| w16 (pairs 1–3) | candidate | 63312 [39080–135702] | 0.946 [0.398–1.500] | 2.116 [0.892–2.746] | 32585 [22885–47882] |
|
||||
| w16 (pairs 4–6) | base | 70553 [35263–125974] | 0.526 [0.246–0.855] | 1.634 [0.778–3.391] | 33664 [19957–45631] |
|
||||
| w16 (pairs 4–6) | candidate | 107407 [88704–116627] | 0.498 [0.477–0.567] | 1.095 [0.869–1.376] | 27828 [22746–32842] |
|
||||
| w16 (all 12) | base | 90596 [35263–125974] | 0.608 [0.447–1.880] | 1.237 [0.761–3.391] | 34642 [19957–45631] |
|
||||
| w16 (all 12) | candidate | 98983 [39080–135702] | 0.550 [0.398–1.500] | 1.138 [0.869–2.746] | 28822 [22746–47882] |
|
||||
|
||||
Allocation, from one JFR `profile` recording per variant on burst32 (sampled weight on `moonbridge-session-io` threads): base 995 MB, candidate 846 MB. The base's per-frame `BinaryWebSocketFrame` (67 MB) is no longer among the candidate's top allocations. In both variants the largest items are the relay's per-frame `DefaultChannelPromise`, `ChannelOutboundBuffer$Entry` and decoder slices, which this change does not touch. The candidate's `StacklessClosedChannelException` sample comes from Netty's channel-close path at the end of each phase, not from the data path. Profiled runs measured 2138–2407 ns/frame for the base and 1597–1807 ns/frame for the candidate.
|
||||
|
||||
Raw output: [2026-10-22-websocket-coalescing-raw.txt](2026-10-22-websocket-coalescing-raw.txt) (lines are `workload variant pair phase: metrics`).
|
||||
|
||||
## Interpretation
|
||||
|
||||
- burst32: median CPU per frame is 21% lower and the ranges do not overlap; frames/s and p50 improved. The criteria are met.
|
||||
- w1: one frame per read, so there is nothing to coalesce. Throughput, latency and CPU ranges overlap; no regression.
|
||||
- w16: very noisy in both variants (base throughput alone spans 35k–126k frames/s). The first three pairs had a worse candidate p50 median with overlapping ranges; the next three, with the start order rotated, reversed every direction. Pooled, the candidate is no worse on any median. Coalescing cannot add waiting: unflushed writes were already held until the same flush.
|
||||
|
||||
## Limits
|
||||
|
||||
One loopback run on one desktop, plain `ws://` (no TLS, so SslHandler's own coalescing was not exercised), synthetic echo traffic, no CDN, no real client mod (whose Netty 4.0.10 decoder now receives fewer, larger messages; its limit is 16 MiB per frame, far above the 64 KiB cap), no cross-host RTT and no production player counts. Single-host loopback numbers are never capacity claims.
|
||||
Reference in new issue
Block a user