You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-19 15:06:19 +00:00
perf: run game packet handlers off the Netty I/O loop + bound A* pathfinding (P2)
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.
This commit is contained in:
+4
-1
@@ -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;
|
||||
|
||||
+15
-1
@@ -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<SocketChannel> {
|
||||
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<SocketChanne
|
||||
|
||||
ch.pipeline().addLast("idleEventHandler", new IdleTimeoutHandler(30, 60));
|
||||
ch.pipeline().addLast(new GameMessageRateLimit());
|
||||
ch.pipeline().addLast(new GameMessageHandler());
|
||||
ch.pipeline().addLast(PACKET_HANDLER_GROUP, "gameMessageHandler", new GameMessageHandler());
|
||||
ch.pipeline().addLast("messageEncoder", new GameServerMessageEncoder());
|
||||
|
||||
if (PacketManager.DEBUG_SHOW_PACKETS) {
|
||||
|
||||
+7
-8
@@ -56,14 +56,13 @@ public class GameMessageHandler extends ChannelInboundHandlerAdapter {
|
||||
ClientMessage message = (ClientMessage) msg;
|
||||
|
||||
try {
|
||||
ChannelReadHandler handler = new ChannelReadHandler(ctx, message);
|
||||
|
||||
if (PacketManager.MULTI_THREADED_PACKET_HANDLING) {
|
||||
Emulator.getThreading().run(handler);
|
||||
return;
|
||||
}
|
||||
|
||||
handler.run();
|
||||
// This handler is registered on a dedicated EventExecutorGroup
|
||||
// (see WebSocketChannelInitializer), so channelRead already runs OFF
|
||||
// the Netty I/O event loop, serialized per channel. Running the
|
||||
// handler inline here keeps that per-channel ordering — submitting to
|
||||
// the shared game pool instead would break ordering, so we no longer
|
||||
// branch on MULTI_THREADED_PACKET_HANDLING.
|
||||
new ChannelReadHandler(ctx, message).run();
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Caught exception", e);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user