From 4eb1484dafe1c531494442c262b5e9464ef57c5c Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 9 Jun 2026 11:36:34 +0000 Subject: [PATCH] perf: run game packet handlers off the Netty I/O loop + bound A* pathfinding (P2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause from the CPU audit: every incoming packet handler ran on the Netty I/O event loop (MULTI_THREADED_PACKET_HANDLING is false by default), so any blocking handler — login DB + loadHabbo, friends/polls/catalog/guild-forum JDBC (~48 handlers), synchronous A* per walk — stalled socket I/O for every other client sharing that I/O thread. - WebSocketChannelInitializer: register GameMessageHandler on a dedicated DefaultEventExecutorGroup (max(16, 2x cores), daemon). Netty pins each channel to one executor in the group, so a client's packets stay strictly ordered (no new intra-client races) while blocking work moves off the I/O loop. The cross-client concurrency degree matches the already-multi-threaded I/O group, and this is strictly safer than the existing (order-losing) shared-pool MULTI_THREADED_PACKET_HANDLING mode the codebase already supported. - GameMessageHandler: always run the handler inline (now on the group thread); drop the shared-pool branch (which would break per-channel ordering and also removes the rejectable-pool ByteBuf-drop path). - PathfinderImpl: default the A* execution-time guard ON (25ms) so a pathological search returns an empty path instead of running unbounded on its thread. Note: this changes the server's packet-threading model — verified to compile, unit-test, and assemble the shaded jar, but should be load-tested before prod. Group size is currently derived from CPU count; can be made a config key if tuning is needed. --- .../rooms/pathfinding/impl/PathfinderImpl.java | 5 ++++- .../gameserver/WebSocketChannelInitializer.java | 16 +++++++++++++++- .../gameserver/decoders/GameMessageHandler.java | 15 +++++++-------- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/pathfinding/impl/PathfinderImpl.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/pathfinding/impl/PathfinderImpl.java index b6bd7df5..c3105f54 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/pathfinding/impl/PathfinderImpl.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/pathfinding/impl/PathfinderImpl.java @@ -24,8 +24,11 @@ public class PathfinderImpl implements Pathfinder { private static final int CACHED_TIMEOUT_MS = Emulator.getConfig() .getInt(CONFIG_EXECUTION_TIME, 25); + // Default ON: bound A* to CACHED_TIMEOUT_MS (25ms) so a pathological search + // can't run unbounded and stall the thread. On timeout findPath returns an + // empty path (the unit simply doesn't move there) — graceful degradation. private static final boolean CACHED_TIMEOUT_ENABLED = Emulator.getConfig() - .getBoolean(CONFIG_TIMEOUT_ENABLED, false); + .getBoolean(CONFIG_TIMEOUT_ENABLED, true); private static final long CACHED_TIMEOUT_NANOS = CACHED_TIMEOUT_MS * 1_000_000L; private final Room room; 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 6418992d..44a1b6be 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 @@ -26,12 +26,26 @@ import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; import io.netty.handler.logging.LoggingHandler; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslHandler; +import io.netty.util.concurrent.DefaultEventExecutorGroup; +import io.netty.util.concurrent.DefaultThreadFactory; +import io.netty.util.concurrent.EventExecutorGroup; import javax.net.ssl.SSLEngine; public class WebSocketChannelInitializer extends ChannelInitializer { private static final int MAX_FRAME_SIZE = 500000; + // Runs the game packet handler OFF the Netty I/O event loop, so a blocking + // handler (login/friends/catalog/guild JDBC, A* pathfinding, etc.) can no + // longer stall socket I/O for every other client sharing that I/O thread. + // A DefaultEventExecutorGroup pins each channel to one executor, so a single + // client's packets stay strictly ordered (no new intra-client races); the + // cross-client concurrency degree is the same the multi-threaded I/O group + // already had. Daemon threads so they don't block JVM shutdown. + private static final EventExecutorGroup PACKET_HANDLER_GROUP = new DefaultEventExecutorGroup( + Math.max(16, Runtime.getRuntime().availableProcessors() * 2), + new DefaultThreadFactory("GamePacketHandler", true)); + private final SslContext sslContext; private final boolean sslEnabled; private final WebSocketServerProtocolConfig wsConfig; @@ -82,7 +96,7 @@ public class WebSocketChannelInitializer extends ChannelInitializer