feat: Adds dynamic thread idle to address CPU usage (#2370)

## Summary
- **Timer-aware idle sleep**: Exposes `Timer.MillisecondsUntilNextTick()` to calculate remaining ms until the next timer wheel tick (0–8ms). The game loop sleeps for that duration minus a 1ms safety margin, instead of spinning at 100% CPU.
- **I/O completion wakeup**: Replaces `Thread.Sleep` with `NetState.WaitForCompletion()`, which uses platform-native completion notification (RIO `RIONotify` on Windows, `eventfd` on Linux, `kevent` timeout on macOS) to wake immediately when network data arrives during sleep.
- **Always-on**: Removes the debug-only `core.enableIdleCPU` config gate. The sleep is self-regulating — under load, `MillisecondsUntilNextTick` returns 0 so no sleep occurs (zero overhead). On idle, CPU drops from ~100% to ~1%.
- **CPS calculation cleanup**: Replaces the 128-element ring buffer with an EMA (exponential moving average) for `CyclesPerSecond`/`AverageCPS` — fewer allocations, no LINQ `.Average()` call each sample.
- **IORingGroup 1.0.6**: Adds `WaitForCompletion(int timeoutMs)` to the `IIORingGroup` interface with platform implementations:
  - **Windows**: `RIONotify` arms the CQ event, `WaitForSingleObject` with timeout
  - **Linux**: `eventfd` registered with io_uring, `poll()` with timeout
  - **macOS**: `kevent()` with timeout

## Test plan
- [ ] Build succeeds on all platforms (`dotnet build`)
- [ ] Empty server: verify CPU usage drops from ~100% to ~1% idle
- [ ] Loaded server: verify no added latency — `MillisecondsUntilNextTick` returns 0 when timers are firing, sleep is skipped
- [ ] Connect a client during idle — verify connection accepted within one timer tick (~8ms)
- [ ] Verify `[admin` gump shows reasonable CPS values (EMA convergence)
This commit is contained in:
Kamron Batman 2026-03-13 23:53:49 -07:00 committed by GitHub
parent 1e4cdc4ab6
commit e3cba66284
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 35 additions and 23 deletions

View file

@ -57,6 +57,16 @@ public partial class NetState
/// </summary>
public static IIORingGroup Ring => _socketManager?.Ring;
/// <summary>
/// Waits for network I/O completions or until the specified timeout expires.
/// Used by the game loop to sleep efficiently while remaining responsive to network events.
/// </summary>
/// <param name="timeoutMs">Maximum time to wait in milliseconds.</param>
public static void WaitForCompletion(int timeoutMs)
{
_socketManager?.WaitForCompletion(timeoutMs);
}
/// <summary>
/// Gets the listening addresses that the server is bound to.
/// </summary>