fix: Fixes tick count wrap-around in movement throttle, and eliminates more allocations in NetState (#2603)
## Summary Removes the per-tick allocation in the movement throttle, fixes tick-count wrap-around bugs in the throttle and RTT probe state, and trims per-connection allocations and dead fields in `NetState`. ## Movement throttle - **No more per-tick `List<NetState>` snapshot.** `ProcessAllQueues()` iterates the `HashSet` directly and removes drained or disconnected states in place. `HashSet<T>.Remove` does not invalidate enumerators on .NET Core 3.0+ (verified on 10.0.11); only inserting a *new* member does, and the only `Add` is in the packet handler, which never nests with `Slice()`. The eager `Remove` calls in `RejectAndReset`, `ClearQueue`, and `ProcessMovementQueue` are gone; membership is reconciled once per tick from `_hasQueuedMovements`. - **Debug logging** is now gated solely by the per-connection `NetState.MovementLogging` flag. The global `movementThrottle.debugLogging` setting is removed. - **New settings**: `movementThrottle.maxRttBonus`, `movementThrottle.maxChainGap`, and `movementThrottle.speedHackNotificationCooldown` were fields with no config binding. ## Tick-count wrap-around All comparisons are now in subtraction form and no tick field uses zero as a sentinel: - `now < _nextMovementTime` in the queue drain loop → `now - _nextMovementTime < 0`. - `_lastMovementRecordTime > 0`, `_lastSpeedHackNotification`, `_rttProbeTime > 0`, and `_nextRttProbe == 0` sentinels replaced with `_hasMovementRecord`, `_speedHackNotified`, `_rttProbePending`, and a seeded `_nextRttProbe`. - `_lastQueueDepthCheck` and `_movementWindowStart` are seeded from `Core.TickCount` at construction and on reset instead of zero. User-visible effects of the old code: on hosts with pass-through counters (GCP) movement history never recorded and speed hack detection was silently off; on every host, staff speed hack notifications were suppressed until `Core.TickCount` exceeded the five-minute cooldown. ## NetState - `Instances` returns `HashSet<NetState>` again so engine-internal `foreach` uses the struct enumerator instead of boxing through `IReadOnlySet<T>`. - Removed `_sustainedQueueDepth` (declared and zeroed since #2266, never read), `_lastRtt` (now derived as `LastRtt` from the newest history slot), and `_rttProbeTimestampHiRes` (only fed one debug log line). 20 bytes per connection. - `HuePickers`, `Menus`, and `Trades` are lazily created instead of allocating three lists per connection, including every login-server connection that dies on shard select. `Trades` is released when it empties. All helpers and the `HuePickerResponse` / `MenuResponse` handlers are null-tolerant; the trade cancel loops keep their `i < Count` guards because `SecureTrade.Cancel()` runs virtual item hooks that can re-enter the same list. ## Testing - `dotnet build -c Release` clean. - All MovementThrottle tests pass (27), plus the Trade / Menu / HuePicker / NetState tests (32).
This commit is contained in:
parent
c9875e7f64
commit
547c2ea0fa
5 changed files with 153 additions and 120 deletions
|
|
@ -15,7 +15,6 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using Server.Logging;
|
||||
|
||||
|
|
@ -70,23 +69,24 @@ public partial class NetState
|
|||
internal Queue<QueuedMovement> _movementQueue; // Lazy initialized
|
||||
internal long _movementCredit; // Credit buffer for timing jitter
|
||||
internal long _nextMovementTime = Core.TickCount; // When next movement is allowed
|
||||
internal int _sustainedQueueDepth; // Tracks sustained high queue depth
|
||||
internal long _lastQueueDepthCheck; // Throttle depth check frequency
|
||||
internal long _lastQueueDepthCheck = Core.TickCount; // Throttle depth check frequency
|
||||
internal bool _hasQueuedMovements; // Fast check for Slice()
|
||||
|
||||
// Movement history for rate-based speed hack detection (lazy initialized)
|
||||
internal MovementRecord[] _movementHistory; // Circular buffer
|
||||
internal int _movementHistoryIndex; // Next write position (also serves as count until full)
|
||||
internal bool _movementHistoryFull; // True once buffer has wrapped
|
||||
internal long _lastMovementRecordTime; // For calculating intervals
|
||||
internal long _lastMovementRecordTime; // For calculating intervals (valid only when _hasMovementRecord)
|
||||
internal bool _hasMovementRecord; // False until the first movement in a chain is seen
|
||||
|
||||
// Detection state
|
||||
internal int _consecutiveHighRateSeconds; // Sustained detection counter
|
||||
internal long _lastSpeedHackNotification; // Rate-limit notifications
|
||||
internal long _lastSpeedHackNotification; // Rate-limit notifications (valid only when _speedHackNotified)
|
||||
internal bool _speedHackNotified; // False until the first notification is sent
|
||||
internal int _lastGapDuration; // Duration of last gap > maxChainGap (for burst forgiveness)
|
||||
|
||||
// Movement packet rate tracking (for speed hack detection)
|
||||
internal long _movementWindowStart; // Start of current 1-second window
|
||||
internal long _movementWindowStart = Core.TickCount; // Start of current 1-second window
|
||||
internal int _movementsInWindow; // Count in current window
|
||||
internal int _peakMovementRate; // Highest rate seen (packets/sec)
|
||||
|
||||
|
|
@ -100,10 +100,9 @@ public partial class NetState
|
|||
_nextMovementTime = Core.TickCount;
|
||||
_movementCredit = 0;
|
||||
_hasQueuedMovements = false;
|
||||
_sustainedQueueDepth = 0;
|
||||
|
||||
// Reset movement history - next movement starts a new chain
|
||||
_lastMovementRecordTime = 0;
|
||||
_hasMovementRecord = false;
|
||||
_movementHistoryIndex = 0;
|
||||
_movementHistoryFull = false;
|
||||
|
||||
|
|
@ -113,7 +112,7 @@ public partial class NetState
|
|||
_rttProbeInterval = RttProbeIntervalNormal;
|
||||
|
||||
// Reset packet rate window
|
||||
_movementWindowStart = 0;
|
||||
_movementWindowStart = Core.TickCount;
|
||||
_movementsInWindow = 0;
|
||||
}
|
||||
|
||||
|
|
@ -165,17 +164,19 @@ public partial class NetState
|
|||
private const long MaxStableLatency = 200; // Max RTT (ms) for "stable" connection
|
||||
|
||||
// RTT state
|
||||
internal long _rttProbeTime; // When we sent the probe (0 = not waiting)
|
||||
internal long _lastRtt; // Most recent RTT measurement
|
||||
internal bool _rttProbePending; // True while waiting for a probe response
|
||||
internal long _rttProbeTime; // When we sent the probe (valid only when _rttProbePending)
|
||||
internal long[] _rttHistory; // Rolling history (lazy init)
|
||||
internal int _rttHistoryIndex; // Current position in history
|
||||
internal int _rttSampleCount; // Number of samples collected (saturates at RttHistorySize)
|
||||
internal long _rttVariance; // Calculated variance for stability
|
||||
internal long _nextRttProbe; // When to send next probe
|
||||
internal long _nextRttProbe = Core.TickCount; // When to send next probe
|
||||
internal int _rttProbeInterval = RttProbeIntervalNormal; // Current probe interval
|
||||
|
||||
// High-resolution timestamp for RTT measurement (Stopwatch ticks, not game loop ticks)
|
||||
private long _rttProbeTimestampHiRes;
|
||||
/// <summary>
|
||||
/// Gets the most recent RTT measurement, or 0 if none has been recorded.
|
||||
/// </summary>
|
||||
public long LastRtt => _rttSampleCount > 0 ? _rttHistory[(_rttHistoryIndex - 1) & (RttHistorySize - 1)] : 0;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the RTT probe interval based on suspicion level.
|
||||
|
|
@ -206,23 +207,22 @@ public partial class NetState
|
|||
var now = Core.TickCount;
|
||||
|
||||
// Don't send if we're still waiting for a response
|
||||
if (_rttProbeTime > 0)
|
||||
if (_rttProbePending)
|
||||
{
|
||||
// Timeout after 10 seconds - connection is probably dead or very laggy
|
||||
if (now - _rttProbeTime > 10000)
|
||||
{
|
||||
_rttProbeTime = 0;
|
||||
_rttProbeTimestampHiRes = 0;
|
||||
_rttProbePending = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// First probe: send immediately when player starts moving
|
||||
// Subsequent probes: send when interval has passed
|
||||
if (_nextRttProbe == 0 || now >= _nextRttProbe)
|
||||
if (now - _nextRttProbe >= 0)
|
||||
{
|
||||
_rttProbePending = true;
|
||||
_rttProbeTime = now;
|
||||
_rttProbeTimestampHiRes = Stopwatch.GetTimestamp();
|
||||
_nextRttProbe = now + _rttProbeInterval + Utility.Random(RttProbeJitter);
|
||||
|
||||
if (_movementLogging)
|
||||
|
|
@ -242,10 +242,9 @@ public partial class NetState
|
|||
/// </summary>
|
||||
public void RecordRttMeasurement()
|
||||
{
|
||||
var nowHiRes = Stopwatch.GetTimestamp();
|
||||
var now = Core.TickCount;
|
||||
|
||||
if (_rttProbeTime <= 0)
|
||||
if (!_rttProbePending)
|
||||
{
|
||||
// Not expecting a response (client-initiated version send) - ignore silently
|
||||
return;
|
||||
|
|
@ -253,19 +252,15 @@ public partial class NetState
|
|||
|
||||
var rtt = now - _rttProbeTime;
|
||||
|
||||
// High-resolution RTT in microseconds
|
||||
var rttHiResUs = (nowHiRes - _rttProbeTimestampHiRes) * 1_000_000 / Stopwatch.Frequency;
|
||||
|
||||
if (_movementLogging)
|
||||
{
|
||||
movementLogger.Debug(
|
||||
"[RTT-Response] {Account}: {Rtt}ms (HiRes: {RttHiRes:F2}ms)",
|
||||
Account?.Username ?? _toString, rtt, rttHiResUs / 1000.0
|
||||
"[RTT-Response] {Account}: {Rtt}ms",
|
||||
Account?.Username ?? _toString, rtt
|
||||
);
|
||||
}
|
||||
|
||||
_rttProbeTime = 0;
|
||||
_rttProbeTimestampHiRes = 0;
|
||||
_rttProbePending = false;
|
||||
|
||||
// Sanity check - RTT should be positive and reasonable
|
||||
if (rtt is <= 0 or > 10000)
|
||||
|
|
@ -285,7 +280,6 @@ public partial class NetState
|
|||
|
||||
// Update history
|
||||
_rttHistory[_rttHistoryIndex++ & (RttHistorySize - 1)] = rtt;
|
||||
_lastRtt = rtt;
|
||||
|
||||
// Track sample count (saturates at buffer size)
|
||||
if (_rttSampleCount < RttHistorySize)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue