From 2653c465d1a059778af62c9e499f7efff812d0f5 Mon Sep 17 00:00:00 2001 From: evilight Date: Sat, 10 Oct 2026 16:23:39 +0800 Subject: [PATCH] world: pace the mob AI and respawn sweeps instead of spinning MobAIThread and MobRespawnThread both ran `while (true)` around a sweep of every zone with no delay at all, so each of them pinned a full core for the lifetime of the world server. Measured in the container: 195% total CPU with tid 229 aiThread 106% of one core tid 231 respawnThread 106% of one core and nothing else above 1%. The 100 ms check in the respawn loop only throttles how often a mob may respawn, it never paused the loop. Both sweeps now run at a fixed rate (AI_TICK_INTERVAL_MS / RESPAWN_TICK_INTERVAL_MS, 100 ms by default) by sleeping the remainder of each tick. MobAI.DetermineAction already gates individual actions on timestamps (lastAttackTime, nextCastTime, patrol delays), so pacing the sweep removes the busy spin without changing the AI behaviour; raise the constants to trade reaction time for CPU. --- src/engine/mobileAI/Threads/MobAIThread.java | 28 +++++++++++++++++++ .../mobileAI/Threads/MobRespawnThread.java | 18 ++++++++++++ 2 files changed, 46 insertions(+) diff --git a/src/engine/mobileAI/Threads/MobAIThread.java b/src/engine/mobileAI/Threads/MobAIThread.java index b3699879..9c1d0a66 100644 --- a/src/engine/mobileAI/Threads/MobAIThread.java +++ b/src/engine/mobileAI/Threads/MobAIThread.java @@ -14,6 +14,21 @@ public class MobAIThread implements Runnable{ public static int AI_PULSE_MOB_THRESHOLD = 200; public static int AI_PATROL_DIVISOR = 15; public static float AI_CAST_FREQUENCY; + + /** + * Fixed-rate pacing for one full AI pass, in milliseconds (default 10 passes + * per second). + * + * The pass used to run with no delay whatsoever - `while (true)` straight into + * a sweep of every zone and every mob - which pinned one core for the entire + * lifetime of the world server (~100% of a core even in an empty world, two + * cores together with MobRespawnThread). Individual actions inside + * MobAI.DetermineAction are already gated by timestamps (lastAttackTime, + * nextCastTime, patrol delays), so running the sweep at a fixed rate removes + * the busy spin without changing behaviour. Raise this value to trade AI + * reaction time for CPU. + */ + public static int AI_TICK_INTERVAL_MS = 100; // Thread constructor public MobAIThread() { @@ -27,6 +42,9 @@ public class MobAIThread implements Runnable{ AI_CAST_FREQUENCY = Float.parseFloat(ConfigManager.MB_AI_CAST_FREQUENCY.getValue()); AI_BASE_AGGRO_RANGE = (int)(60 * Float.parseFloat(ConfigManager.MB_AI_AGGRO_RANGE.getValue())); while (true) { + + long tickStart = System.currentTimeMillis(); + for (Zone zone : ZoneManager.getAllZones()) { for (Mob mob : zone.zoneMobSet) { @@ -40,6 +58,16 @@ public class MobAIThread implements Runnable{ } } } + + long sleep = AI_TICK_INTERVAL_MS - (System.currentTimeMillis() - tickStart); + + if (sleep > 0) { + try { + Thread.sleep(sleep); + } catch (InterruptedException ie) { + return; + } + } } } public static void startAIThread() { diff --git a/src/engine/mobileAI/Threads/MobRespawnThread.java b/src/engine/mobileAI/Threads/MobRespawnThread.java index 24e2f281..aa5d3334 100644 --- a/src/engine/mobileAI/Threads/MobRespawnThread.java +++ b/src/engine/mobileAI/Threads/MobRespawnThread.java @@ -25,6 +25,13 @@ import org.pmw.tinylog.Logger; public class MobRespawnThread implements Runnable { + /** + * Fixed-rate pacing for one respawn sweep, in milliseconds. The sweep used to + * run with no delay at all, pinning a core for the lifetime of the world server + * (the 100 ms check below only throttles how often a mob may actually respawn, + * it never paused the loop). Keep this at or below the 100 ms respawn throttle. + */ + public static int RESPAWN_TICK_INTERVAL_MS = 100; public MobRespawnThread() { Logger.info(" MobRespawnThread thread has started!"); @@ -36,6 +43,8 @@ public class MobRespawnThread implements Runnable { while (true) { + long tickStart = System.currentTimeMillis(); + try { for (Zone zone : ZoneManager.getAllZones()) { @@ -55,6 +64,15 @@ public class MobRespawnThread implements Runnable { Logger.error(e); } + long sleep = RESPAWN_TICK_INTERVAL_MS - (System.currentTimeMillis() - tickStart); + + if (sleep > 0) { + try { + Thread.sleep(sleep); + } catch (InterruptedException ie) { + return; + } + } } } public static void startRespawnThread() {