The loop span through its body regardless of whether there was anything to do -- ~10% of a desktop core for an empty shard, ~70% of a small VPS core, and a process that never idles is exactly what burstable vCPU plans throttle. The loop now blocks in NetState.WaitForCompletion whenever every queue it drains is empty, waking on the next timer tick or the moment work arrives. Receive completions, new connections, and cross-thread LoopContext.Post (via IORingGroup 1.0.10's sticky Wake) are all in the wait set, so sleeping adds no latency to any of them; only timer-driven logic sees wheel lag, bounded by server.eventLoopIdleWaitMs (default 2ms, 0 = never sleep). Measured on a real world of 190k items / 33k mobiles: 10.4% of a core to 0.8-1.0%, with peak tick lag unchanged. Spin mode independently gained 7x the iterations per core from the ring's AcceptEx rework. Sleeping also gives the GC natural pause points, which the old spin loop denied it -- memory no longer climbs until a save forces a collection. A sleep is bounded by the next wheel turn, so a correctly honoured sleep can never miss a deadline; the only way sleeping harms the wheel is the host returning the wait late. That overshoot is measured on every sleep, and an escalating backoff (server.lateWakeThreshold) suspends sleeping when it persists -- server work like saves or heavy commands cannot trip it by construction. Hosts without high-resolution waits are detected once at startup and spin instead. The admin gump shows the verdict instead of the now-meaningless CPS figure, which is removed. Time accounting for diagnosis is compiled out of normal builds: build with -p:EventLoopProfiling=true to enable EventLoopProfiler (per-phase wall time, sleep overshoot, GC pauses, stolen-time residual, ~15min ring buffer) and the [LoopStats command with CSV dump. See dev-docs/debugging-event-loop.md for the diagnosis funnel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
334 lines
10 KiB
C#
334 lines
10 KiB
C#
/*************************************************************************
|
|
* ModernUO *
|
|
* Copyright 2019-2026 - ModernUO Development Team *
|
|
* Email: hi@modernuo.com *
|
|
* File: Timer.TimerWheel.cs *
|
|
* *
|
|
* This program is free software: you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation, either version 3 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
* You should have received a copy of the GNU General Public License *
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
*************************************************************************/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Runtime.CompilerServices;
|
|
|
|
namespace Server;
|
|
|
|
public partial class Timer
|
|
{
|
|
#if DEBUG_TIMERS
|
|
private const int _chainExecutionThreshold = 512;
|
|
#endif
|
|
private const int _ringSizePowerOf2 = 12;
|
|
private const int _ringSize = 1 << _ringSizePowerOf2; // 4096
|
|
private const int _ringLayers = 3;
|
|
private const int _tickRatePowerOf2 = 3;
|
|
private const int _tickRate = 1 << _tickRatePowerOf2; // 8ms
|
|
private const long _maxDuration = (long)_tickRate << (_ringSizePowerOf2 * _ringLayers - 1);
|
|
|
|
private static readonly Timer[][] _rings = new Timer[_ringLayers][];
|
|
private static readonly int[] _ringIndexes = new int[_ringLayers];
|
|
private static readonly Timer[] _executingRings = new Timer[_ringLayers];
|
|
|
|
private static long _lastTickTurned = -1;
|
|
|
|
public static void Init(long tickCount)
|
|
{
|
|
_lastTickTurned = tickCount;
|
|
|
|
for (var i = 0; i < _rings.Length; i++)
|
|
{
|
|
_rings[i] = new Timer[_ringSize];
|
|
_ringIndexes[i] = 0;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Milliseconds of simulated time one wheel turn advances.
|
|
/// </summary>
|
|
public static int TickRate => _tickRate;
|
|
|
|
public static void Slice(long tickCount)
|
|
{
|
|
EventLoopProfiler.WheelSlice(tickCount - _lastTickTurned);
|
|
|
|
var deltaSinceTurn = tickCount - _lastTickTurned;
|
|
while (deltaSinceTurn >= _tickRate)
|
|
{
|
|
deltaSinceTurn -= _tickRate;
|
|
_lastTickTurned += _tickRate;
|
|
Turn();
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static long MillisecondsUntilNextTick(long tickCount) => Math.Max(0, _tickRate - (tickCount - _lastTickTurned));
|
|
|
|
private static void Turn()
|
|
{
|
|
var turnNextWheel = false;
|
|
|
|
// Detach the chain from the timer wheel. This allows adding timers to the same slot during execution.
|
|
for (var i = 0; i < _ringLayers; i++)
|
|
{
|
|
if (i == 0 || turnNextWheel)
|
|
{
|
|
var ringIndex = ++_ringIndexes[i];
|
|
turnNextWheel = ringIndex >= _ringSize;
|
|
|
|
if (turnNextWheel)
|
|
{
|
|
ringIndex = _ringIndexes[i] = 0;
|
|
}
|
|
|
|
_executingRings[i] = _rings[i][ringIndex];
|
|
_rings[i][ringIndex] = null;
|
|
}
|
|
else
|
|
{
|
|
_executingRings[i] = null;
|
|
}
|
|
}
|
|
|
|
for (var i = 0; i < _ringLayers; i++)
|
|
{
|
|
#if DEBUG_TIMERS
|
|
var executionCount = 0;
|
|
#endif
|
|
while (_executingRings[i] != null)
|
|
{
|
|
#if DEBUG_TIMERS
|
|
executionCount++;
|
|
#endif
|
|
|
|
var timer = _executingRings[i];
|
|
|
|
// Set the executing timer to the next in the link list because we will be detaching.
|
|
_executingRings[i] = timer._nextTimer;
|
|
|
|
timer.Detach();
|
|
|
|
// Check to see if it's running just in case it was stopped by another timer
|
|
if (timer.Running)
|
|
{
|
|
if (i > 0 && timer._remaining > 0)
|
|
{
|
|
// Promote
|
|
AddTimer(timer, timer._remaining);
|
|
}
|
|
else
|
|
{
|
|
Execute(timer);
|
|
}
|
|
}
|
|
}
|
|
#if DEBUG_TIMERS
|
|
if (executionCount > _chainExecutionThreshold)
|
|
{
|
|
logger.Warning(
|
|
"Timer threshold of {Threshold} met. Executed {Count} timers sequentially.",
|
|
_chainExecutionThreshold,
|
|
executionCount
|
|
);
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
private static void Execute(Timer timer)
|
|
{
|
|
var finished = timer.Count != 0 && timer.Index + 1 >= timer.Count;
|
|
|
|
// Stop the timer from running so that way if Start() is called in OnTick, the timer will be started.
|
|
if (finished)
|
|
{
|
|
timer.InternalStop();
|
|
timer.Version++;
|
|
}
|
|
|
|
var version = timer.Version;
|
|
|
|
timer.OnTick();
|
|
|
|
// Starting doesn't change the timer version, so we need to check if it's finished and if it's still running.
|
|
if (timer.Version != version || finished && timer.Running)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!finished)
|
|
{
|
|
AddTimer(timer, (long)timer.Interval.TotalMilliseconds);
|
|
}
|
|
else
|
|
{
|
|
// Already stopped and detached, now run OnDetach
|
|
timer.OnDetach();
|
|
}
|
|
|
|
timer.Index++;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static long RoundTicksToNextPowerOfTwo(long value)
|
|
{
|
|
if (value <= 0)
|
|
{
|
|
return _tickRate;
|
|
}
|
|
|
|
const long mask = _tickRate - 1;
|
|
return (value + mask) & ~mask;
|
|
}
|
|
|
|
private static void AddTimer(Timer timer, long delay)
|
|
{
|
|
var actualDelay = delay;
|
|
|
|
var resolutionPowerOf2 = _tickRatePowerOf2;
|
|
for (var i = 0; i < _ringLayers; i++)
|
|
{
|
|
var resolution = 1L << resolutionPowerOf2;
|
|
var nextResolutionPowerOf2 = resolutionPowerOf2 + _ringSizePowerOf2;
|
|
var max = 1L << nextResolutionPowerOf2;
|
|
var lastRing = i == _ringLayers - 1;
|
|
|
|
if (delay < max || lastRing)
|
|
{
|
|
var ringIndex = _ringIndexes[i];
|
|
var remaining = delay & (resolution - 1);
|
|
var slot = (delay >> resolutionPowerOf2) + ringIndex + (remaining > 0 ? 1 : 0);
|
|
|
|
// Round up if we have a delay of 0
|
|
if (delay == 0)
|
|
{
|
|
slot++;
|
|
remaining = 0;
|
|
}
|
|
|
|
if (slot >= _ringSize)
|
|
{
|
|
slot -= _ringSize;
|
|
|
|
// Slot should only be more than 4096 if we are on the last ring and the timer is more than max capacity
|
|
// In this case, we will just throw it on the last slot.
|
|
if (lastRing && slot > _ringSize)
|
|
{
|
|
logger.Error(
|
|
$"Timer {{Timer}} has a duration of {{Duration}}ms, more than max capacity of {{MaxDuration}}ms.{Environment.NewLine}{{StackTrace}}",
|
|
timer.GetType(),
|
|
actualDelay,
|
|
_maxDuration,
|
|
new StackTrace()
|
|
);
|
|
|
|
slot = Math.Max(0, ringIndex - 1);
|
|
}
|
|
}
|
|
|
|
timer.Next = Core.Now + timer.Delay;
|
|
timer.Attach(_rings[i][slot]);
|
|
timer._remaining = remaining;
|
|
timer._ring = i;
|
|
timer._slot = (int)slot;
|
|
|
|
_rings[i][slot] = timer;
|
|
return;
|
|
}
|
|
|
|
// The remaining amount until we turn this ring
|
|
var offsetDelay = resolution * (_ringSize - _ringIndexes[i]);
|
|
delay -= offsetDelay;
|
|
resolutionPowerOf2 = nextResolutionPowerOf2;
|
|
}
|
|
}
|
|
|
|
public static void DumpInfo(TextWriter tw)
|
|
{
|
|
tw.WriteLine($"Date: {Core.Now.ToLocalTime()}{Environment.NewLine}");
|
|
tw.WriteLine($"Pool - Count: {_poolCount}; Capacity {_poolCapacity}{Environment.NewLine}");
|
|
|
|
var total = 0.0;
|
|
var hash = new Dictionary<string, int>();
|
|
|
|
for (var i = 0; i < _ringLayers; i++)
|
|
{
|
|
for (var j = 0; j < _ringSize; j++)
|
|
{
|
|
var t = _rings[i][j];
|
|
if (t == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
while (t != null)
|
|
{
|
|
var name = t.ToString();
|
|
|
|
hash.TryGetValue(name, out var count);
|
|
hash[name] = count + 1;
|
|
|
|
total++;
|
|
|
|
t = t._nextTimer;
|
|
}
|
|
}
|
|
}
|
|
|
|
tw.WriteLine("Timers:");
|
|
|
|
foreach (var (name, count) in hash.OrderByDescending(o => o.Value))
|
|
{
|
|
var percent = count / total;
|
|
var line = $"{count:#,0} ({percent:P1})";
|
|
// 6 - 15 / 8 = 1
|
|
var tabs = new string('\t', line.Length < 12 ? 2 : 1);
|
|
tw.WriteLine($"{line}{tabs}{name}");
|
|
}
|
|
|
|
#if DEBUG_TIMERS
|
|
tw.WriteLine($"{Environment.NewLine}Stack Traces:");
|
|
foreach (var kvp in DelayCallTimer._stackTraces)
|
|
{
|
|
tw.WriteLine(kvp.Value);
|
|
tw.WriteLine();
|
|
}
|
|
#endif
|
|
|
|
tw.WriteLine();
|
|
tw.WriteLine();
|
|
}
|
|
|
|
public static void ClearAllTimers(long tickCount)
|
|
{
|
|
_lastTickTurned = tickCount;
|
|
|
|
foreach (var t in _rings)
|
|
{
|
|
if (t == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
for (var i = 0; i < _ringSize; i++)
|
|
{
|
|
var node = t[i];
|
|
Timer next;
|
|
|
|
do
|
|
{
|
|
next = node?._nextTimer;
|
|
node?.Stop();
|
|
} while (next != null);
|
|
}
|
|
}
|
|
}
|
|
}
|