diff --git a/Projects/Server/Main.cs b/Projects/Server/Main.cs index ce0097990..608256651 100644 --- a/Projects/Server/Main.cs +++ b/Projects/Server/Main.cs @@ -39,21 +39,19 @@ public static class Core { private static readonly ILogger logger = LogFactory.GetLogger(typeof(Core)); - // Written from other threads (Kill, RequestSnapshot) and read by the event loop. Volatile - // because the loop now genuinely blocks between reads rather than spinning past them. + // Written off-loop (Kill, RequestSnapshot); volatile because the loop blocks between reads. private static volatile bool _performProcessKill; private static bool _restartOnKill; private static volatile bool _performSnapshot; private static string _snapshotPath; - // A backstop, not a latency control: the wheel's tick rate bounds the sleep, so this only - // limits the damage if a wake signal is ever missed. Measured across 1/2/4/8ms, 2 is optimal. + // A backstop, not a latency control: the wheel's tick rate already bounds the sleep. + // Measured across 1/2/4/8ms; 2 is optimal. private static int _eventLoopIdleWaitMs = 2; /// - /// Longest the loop will block while idle, in milliseconds. 0 disables idle sleeping, - /// leaving the loop to spin; the adaptive backoff does the same thing temporarily when the - /// host keeps returning waits late. + /// Longest the loop will block while idle, in milliseconds. 0 spins instead; the backoff + /// does the same temporarily when the host keeps returning waits late. /// public static int EventLoopIdleWaitMs => _eventLoopIdleWaitMs; @@ -74,9 +72,8 @@ public static class Core private const long HealthSampleIntervalMs = 1000; - // Backoff escalates by doubling: a fixed suspension oscillates forever on a persistently bad - // host, while doubling converges on "stop sleeping" within minutes yet still recovers from a - // transient problem. + // Doubling: a fixed suspension oscillates forever on a persistently bad host, while doubling + // converges on "stop sleeping" yet still recovers from a transient. private const long BackoffBaseMs = 5000; private const long BackoffMaxMs = 120_000; private const int BackoffMaxShift = 5; @@ -84,19 +81,14 @@ public static class Core // Clean streak that clears the escalation. private const long BackoffResetAfterCleanMs = 60_000; - // Backoffs below this are still recoverable and not something an operator can act on, so they - // are logged at Debug. Past it the host has stayed bad through several doublings and is worth - // a Warning; the ceiling above escalates to Error. + // Below this a backoff is still recoverable and not actionable, so it only logs at Debug. private const int WarnAfterConsecutiveBackoffs = 3; - // A sleep is bounded by the time to the next wheel turn, so a correctly honoured sleep can - // never miss a deadline; the only way sleeping harms the wheel is the wait returning late - // (the host descheduled the process). That overshoot is measured per sleep, which is why - // server work -- saves, heavy commands, deep timer callbacks -- cannot trip this backoff. - // Loop-thread only, so plain increments are safe. + // A sleep is bounded by the next wheel turn, so only a wait returning late can cost a deadline. + // Measured per sleep, which is why server work (saves, heavy commands) cannot trip the backoff. private static int _lateWakes; - // Denominator for the late-wake rate: sleeps actually performed in the current sample window. + // Denominator for the late-wake rate. private static int _sleepAttempts; private static long _nextHealthSample; @@ -128,11 +120,8 @@ public static class Core _lateWakes = 0; _sleepAttempts = 0; - // A long clean streak resets the escalation, re-arming the ceiling Error so a host that - // recovers and later degrades again gets re-reported. Checked on every sample rather than - // only on bad ones, so a host that recovers for good still says so instead of waiting to - // degrade again. Gated on the count rather than a "_lastBackoffAt > 0" sentinel because - // tick counts are not guaranteed positive. + // A clean streak resets the escalation and re-arms the ceiling Error. Gated on the count + // rather than a "_lastBackoffAt > 0" sentinel because tick counts are not guaranteed positive. if (_consecutiveBackoffs > 0 && _tickCount - _lastBackoffAt > BackoffResetAfterCleanMs) { if (_consecutiveBackoffs >= WarnAfterConsecutiveBackoffs) @@ -153,20 +142,17 @@ public static class Core return; } - // Lateness is a rate, not a count. An idle loop performs hundreds of ~2ms sleeps a second, - // so a handful of outliers -- a co-tenant burst, a page fault, another process changing the - // system timer resolution -- is normal on a perfectly healthy host. A host that genuinely - // cannot schedule the process returns *most* of its waits late, which is two orders of - // magnitude away. The absolute threshold stays on as a floor for windows with few sleeps, - // where a proportion means nothing. + // Lateness is a rate: an idle loop sleeps hundreds of times a second, so a few outliers are + // normal, while a host that cannot schedule the process returns most of its waits late. The + // threshold above is the floor for windows with too few sleeps for a proportion to mean anything. if (late * 100 < sleeps * _lateWakePercent) { _consecutiveBadSamples = 0; return; } - // Require the condition to persist: any host can drop one sample to unrelated load, and a - // host that is genuinely oversubscribed stays that way, so it trips on the second sample. + // Require persistence: any host can drop one sample to unrelated load, but an oversubscribed + // one stays bad. if (++_consecutiveBadSamples < 2) { return; @@ -185,7 +171,7 @@ public static class Core if (_currentBackoffMs >= BackoffMaxMs) { - // Escalation has run out of room; say so once in terms the operator can act on. + // Escalation has run out of room; say so once. if (!_loggedBackoffCeiling) { _loggedBackoffCeiling = true; @@ -200,9 +186,8 @@ public static class Core return; } - // Each backoff doubles the suspension, so every one of these is a distinct escalation step - // and needs no further rate limiting. The first couple are recoverable and not actionable, - // so they stay at Debug; only a host that survives several doublings earns a Warning. + // Each backoff doubles the suspension, so every line is a distinct escalation step and + // needs no further rate limiting. if (_consecutiveBackoffs < WarnAfterConsecutiveBackoffs) { logger.Debug( @@ -414,8 +399,8 @@ public static class Core _restartOnKill = restart; _performProcessKill = true; - // Callers are usually off-loop (console input, signal handlers). Without this the loop - // would not notice the request until it woke for some other reason. + // Callers are usually off-loop (console input, signal handlers); wake so the request + // is noticed now rather than whenever the loop next surfaces. NetState.Wake(); } @@ -618,9 +603,8 @@ public static class Core _eventLoopIdleWaitMs = Math.Max(0, idleWaitMs); - // Floor for the late-wake backoff: idle waits per second the host may return a full tick - // (8ms) late before the rate test below even applies. Raise to tolerate a jittery host; set - // very high to disable the backoff. + // Floor for the backoff: idle waits per second the host may return a full tick late before + // the rate test below applies at all. Set very high to disable the backoff. var lateWakeThreshold = ServerConfiguration.GetSetting("server.lateWakeThreshold", 1); if (lateWakeThreshold < 0) { @@ -632,10 +616,8 @@ public static class Core _lateWakeThreshold = Math.Max(0, lateWakeThreshold); - // Share of a second's idle waits that must come back late before the backoff trips. A bare - // count cannot separate a few tail outliers from a host that never schedules us -- an idle - // loop sleeps hundreds of times a second -- but the proportion can. 0 leaves the absolute - // threshold in sole charge. + // Share of a second's idle waits that must return late before the backoff trips. 0 leaves + // the threshold above in sole charge. var lateWakePercent = ServerConfiguration.GetSetting("server.lateWakePercent", 10); if (lateWakePercent is < 0 or > 100) { @@ -664,10 +646,8 @@ public static class Core AssemblyHandler.LoadAssemblies(assemblyFiles); - // First-boot interactive setup. Runs after assemblies are loaded (so content can - // register prompts) but before any Serilog output, so console prompts are not - // interleaved with the async console sink. Handlers self-gate on first-boot state - // (e.g. "is my setting already present?"). + // First-boot interactive setup. After assemblies load so content can register prompts, + // before any Serilog output so prompts are not interleaved with the async console sink. AssemblyHandler.Invoke("ConfigurePrompts"); logger.Information("Running on {Framework}", RuntimeInformation.FrameworkDescription); @@ -677,9 +657,8 @@ public static class Core _now = DateTime.UtcNow; _firstTick = _tickCount = GetTimestamp(); - // Seed schedule state from the first real tick: tick counts are not guaranteed to start - // anywhere near zero (hypervisor pass-through counters), so zero-initialized deadlines - // would compare wrong. See dev-docs/tick-counts.md. + // Seed from a real tick: tick counts need not start near zero, so a zero-initialized + // deadline compares wrong. See dev-docs/tick-counts.md. _nextHealthSample = _tickCount + HealthSampleIntervalMs; _idleSleepSuspendedUntil = _tickCount; @@ -700,9 +679,8 @@ public static class Core PingServer.Start(); EventSink.InvokeServerStarted(); - // Without a high-resolution wait a 2ms request quantises to 15.625ms and the loop would - // quietly run a tick behind; spinning is the lesser evil and must not be silent. Only - // fires when both the ring's high-res timer and its timeBeginPeriod fallback failed. + // Without a high-resolution wait a 2ms request quantises to 15.625ms and the loop runs a + // tick behind. Only fires when the high-res timer and the timeBeginPeriod fallback both failed. if (_eventLoopIdleWaitMs > 0 && NetState.Ring?.SupportsHighResolutionWait == false) { logger.Error( @@ -719,8 +697,7 @@ public static class Core /// /// True when every queue the loop drains is empty, so sleeping cannot strand pending work. - /// The drains are bounded (ProcessDeltaQueue stops at the count seen on entry, ExecuteTasks - /// at its per-frame cap), so leftovers are normal and must keep the loop awake. + /// The drains are bounded, so leftovers are normal and must keep the loop awake. /// private static bool IsIdle() => !Mobile.HasQueuedDeltas && !Item.HasQueuedDeltas && LoopContext.IsEmpty && NetState.IsIdle; @@ -779,18 +756,17 @@ public static class Core if (_eventLoopIdleWaitMs > 0 && _tickCount - _idleSleepSuspendedUntil >= 0 && IsIdle()) { - // Re-read the clock: the loop body consumed real time, and a stale timestamp - // would overstate the time to the next tick and sleep straight past it. + // Re-read the clock: a stale timestamp overstates the time to the next tick + // and sleeps straight past it. var start = GetTimestamp(); var due = Timer.MillisecondsUntilNextTick(start); if (due > 0) { var requested = (int)Math.Min(due, _eventLoopIdleWaitMs); - // The GC deliberately collects during idle sleeps -- that is the natural - // pause point it looks for -- so its pauses land here by design and must - // not be charged to the host. Gen1 and above are the only ones whose pause - // approaches a tick; CollectionCount(1) counts those and gen2 with it. + // The GC prefers to collect during idle sleeps, so its pauses land here by + // design and are not the host's fault. Gen1 and above (what + // CollectionCount(1) counts) are the only pauses long enough to reach a tick. var collections = GC.CollectionCount(1); NetState.WaitForCompletion(requested); @@ -799,10 +775,8 @@ public static class Core EventLoopProfiler.SleepEnd(requested, elapsed); _sleepAttempts++; - // A sleep is bounded by the next wheel turn, so only a wait the host - // returned late can cost the wheel a deadline. The second collection read - // is short-circuited behind the overshoot test, so the common path pays for - // one counter read, not two. + // The second collection read sits behind the overshoot test, so the common + // path reads the counter once, not twice. if (elapsed - requested >= Timer.TickRate && GC.CollectionCount(1) == collections) { _lateWakes++; @@ -825,8 +799,7 @@ public static class Core _snapshotPath = snapshotPath; _performSnapshot = true; - // Save requests arrive off-loop. Wake so the snapshot starts now rather than after the - // loop happens to surface for another reason. + // Save requests arrive off-loop; wake so the snapshot starts now. NetState.Wake(); }