From af82352f24d38cc267de4bfd6ad6d46840f5e395 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 9 Jun 2026 16:05:16 +0000 Subject: [PATCH] feat: configurable pool sizes (#2) + pool-safe buffers and opt-in pooled allocator (#5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #2 — tunable thread pools (sensible defaults kept): - io.packet.handler.threads overrides the packet-handler EventExecutorGroup size (default max(16, 2x cores)). - auth.http.pool.size overrides the auth HTTP pool max threads (default 16). #5 — Netty buffer pooling: - Make the crypto handlers pool-safe: GameByteEncryption/GameByteDecryption no longer call ByteBuf.array() on a readBytes-derived buffer (whose arrayOffset is non-zero under a pooled allocator, which would have read/encrypted the wrong region). They now copy the readable region into a plain byte[] (offset-safe) and wrap the result — also drops one intermediate buffer allocation. This is correct for the current unpooled allocator too. (ServerMessage uses its own Unpooled buffer, and ClientMessage reads via buffer methods, so both are already offset-safe.) - Add a shared channel allocator selected by io.netty.allocator.pooled (default false = unpooled-heap, unchanged). Set true for a pooled HEAP allocator (preferDirect=false, so array-backed paths keep working) to cut per-packet alloc/GC churn. Opt-in until validated under load with the Netty leak detector, since unreleased pooled buffers accumulate rather than being GC-reclaimed. New optional config keys (insert into emulator_settings to set/silence the "key not found" notice): io.packet.handler.threads, auth.http.pool.size, io.netty.allocator.pooled. --- .../java/com/eu/habbo/networking/Server.java | 29 ++++++++++++++++++- .../networking/gameserver/GameServer.java | 2 +- .../WebSocketChannelInitializer.java | 13 ++++++++- .../gameserver/auth/AuthHttpHandler.java | 14 ++++++++- .../decoders/GameByteDecryption.java | 14 +++++---- .../encoders/GameByteEncryption.java | 14 +++++---- 6 files changed, 72 insertions(+), 14 deletions(-) diff --git a/Emulator/src/main/java/com/eu/habbo/networking/Server.java b/Emulator/src/main/java/com/eu/habbo/networking/Server.java index 7ae6f43f..4062141c 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/Server.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/Server.java @@ -1,6 +1,9 @@ package com.eu.habbo.networking; +import com.eu.habbo.Emulator; import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.PooledByteBufAllocator; import io.netty.buffer.UnpooledByteBufAllocator; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelOption; @@ -18,6 +21,30 @@ public abstract class Server { private static final Logger LOGGER = LoggerFactory.getLogger(Server.class); + private static volatile ByteBufAllocator sharedAllocator; + + /** + * Shared channel allocator. Defaults to unpooled-heap (the long-standing + * behaviour); set {@code io.netty.allocator.pooled=true} to switch to a + * pooled HEAP allocator (preferDirect=false, so the array-backed crypto + * paths keep working) which removes the per-packet alloc/GC churn. Opt-in + * until validated under load with the Netty leak detector, since pooled + * buffers that aren't released accumulate instead of being GC-reclaimed. + */ + protected static ByteBufAllocator allocator() { + if (sharedAllocator == null) { + synchronized (Server.class) { + if (sharedAllocator == null) { + boolean pooled = Emulator.getConfig() != null + && "true".equalsIgnoreCase(Emulator.getConfig().getValue("io.netty.allocator.pooled", "false")); + sharedAllocator = pooled ? new PooledByteBufAllocator(false) : new UnpooledByteBufAllocator(false); + LOGGER.info("Netty ByteBuf allocator: {}", pooled ? "pooled-heap" : "unpooled-heap"); + } + } + } + return sharedAllocator; + } + protected final ServerBootstrap serverBootstrap; protected final EventLoopGroup bossGroup; protected final EventLoopGroup workerGroup; @@ -45,7 +72,7 @@ public abstract class Server { this.serverBootstrap.childOption(ChannelOption.SO_REUSEADDR, true); this.serverBootstrap.childOption(ChannelOption.SO_RCVBUF, 4096); this.serverBootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(4096)); - this.serverBootstrap.childOption(ChannelOption.ALLOCATOR, new UnpooledByteBufAllocator(false)); + this.serverBootstrap.childOption(ChannelOption.ALLOCATOR, allocator()); } public void connect() { diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/GameServer.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/GameServer.java index b3f81c91..fe0939e5 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/GameServer.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/GameServer.java @@ -84,7 +84,7 @@ public class GameServer extends Server { this.webSocketBootstrap.childOption(ChannelOption.SO_REUSEADDR, true); this.webSocketBootstrap.childOption(ChannelOption.SO_RCVBUF, 4096); this.webSocketBootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(4096)); - this.webSocketBootstrap.childOption(ChannelOption.ALLOCATOR, new UnpooledByteBufAllocator(false)); + this.webSocketBootstrap.childOption(ChannelOption.ALLOCATOR, allocator()); this.webSocketBootstrap.childHandler(wsInitializer); ChannelFuture wsFuture = this.webSocketBootstrap.bind(wsHost, wsPort); diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/WebSocketChannelInitializer.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/WebSocketChannelInitializer.java index 44a1b6be..a5ca2c87 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/WebSocketChannelInitializer.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/WebSocketChannelInitializer.java @@ -43,9 +43,20 @@ public class WebSocketChannelInitializer extends ChannelInitializer 0 ? configured : fallback; + } + private final SslContext sslContext; private final boolean sslEnabled; private final WebSocketServerProtocolConfig wsConfig; diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AuthHttpHandler.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AuthHttpHandler.java index bf1e9369..0944b6b6 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AuthHttpHandler.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AuthHttpHandler.java @@ -34,8 +34,9 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter { // BCrypt, JDBC, the Turnstile HTTPS round-trip and SMTP — running that on the // Netty event loop stalls every client on the same worker. A SEPARATE pool // (not the shared game ThreadPooling) also keeps it from starving room cycles. + private static final int AUTH_POOL_MAX = authPoolMax(); private static final ThreadPoolExecutor AUTH_EXECUTOR = new ThreadPoolExecutor( - 4, 16, 60L, TimeUnit.SECONDS, + Math.min(4, AUTH_POOL_MAX), AUTH_POOL_MAX, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(512), new java.util.concurrent.ThreadFactory() { private final AtomicInteger counter = new AtomicInteger(1); @@ -47,6 +48,17 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter { } }); + // Max threads for the auth pool. Defaults to 16; set the optional + // `auth.http.pool.size` config key to override. + private static int authPoolMax() { + int fallback = 16; + if (com.eu.habbo.Emulator.getConfig() == null) { + return fallback; + } + int configured = com.eu.habbo.Emulator.getConfig().getInt("auth.http.pool.size", fallback); + return configured > 0 ? configured : fallback; + } + static final String LOGIN_PATH = "/api/auth/login"; static final String REGISTER_PATH = "/api/auth/register"; static final String FORGOT_PATH = "/api/auth/forgot-password"; diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/decoders/GameByteDecryption.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/decoders/GameByteDecryption.java index 126648ed..6845206e 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/decoders/GameByteDecryption.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/decoders/GameByteDecryption.java @@ -2,6 +2,7 @@ package com.eu.habbo.networking.gameserver.decoders; import com.eu.habbo.networking.gameserver.GameServerAttributes; import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageDecoder; @@ -15,14 +16,17 @@ public class GameByteDecryption extends ByteToMessageDecoder { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) { - // Read all available bytes. - ByteBuf data = in.readBytes(in.readableBytes()); + // Copy the readable region into a plain array (offset-safe, so this is + // correct for pooled buffers too — buf.array() would have read the wrong + // region for a pooled/sliced buffer). + byte[] bytes = new byte[in.readableBytes()]; + in.readBytes(bytes); - // Decrypt. - ctx.channel().attr(GameServerAttributes.CRYPTO_CLIENT).get().parse(data.array()); + // Decrypt in place. + ctx.channel().attr(GameServerAttributes.CRYPTO_CLIENT).get().parse(bytes); // Continue in the pipeline. - out.add(data); + out.add(Unpooled.wrappedBuffer(bytes)); } } diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/encoders/GameByteEncryption.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/encoders/GameByteEncryption.java index d2930c52..e7a9a09b 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/encoders/GameByteEncryption.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/encoders/GameByteEncryption.java @@ -2,6 +2,7 @@ package com.eu.habbo.networking.gameserver.encoders; import com.eu.habbo.networking.gameserver.GameServerAttributes; import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelOutboundHandlerAdapter; import io.netty.channel.ChannelPromise; @@ -14,16 +15,19 @@ public class GameByteEncryption extends ChannelOutboundHandlerAdapter { // convert to Bytebuf ByteBuf in = (ByteBuf) msg; - // read available bytes - ByteBuf data = (in).readBytes(in.readableBytes()); + // Copy the readable region into a plain array (respects readerIndex / + // arrayOffset, so this is correct for pooled buffers too — buf.array() + // would have returned the wrong region for a pooled/sliced buffer). + byte[] bytes = new byte[in.readableBytes()]; + in.readBytes(bytes); //release old object ReferenceCountUtil.release(in); - // Encrypt. - ctx.channel().attr(GameServerAttributes.CRYPTO_SERVER).get().parse(data.array()); + // Encrypt in place. + ctx.channel().attr(GameServerAttributes.CRYPTO_SERVER).get().parse(bytes); // Continue in the pipeline. - ctx.write(data, promise); + ctx.write(Unpooled.wrappedBuffer(bytes), promise); } }