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.
This commit is contained in:
evilight
2026-10-10 16:23:39 +08:00
parent 97eff6517e
commit 2653c465d1
2 changed files with 46 additions and 0 deletions
@@ -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() {
@@ -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() {