ModernUO/Projects
Kamron Batman 8e5b7af1cf
perf: retire auto-denylist holds from an expiry ring instead of scanning
Reclaiming lapsed holds was O(entries held). Every cap-triggered reclaim
during a flood, and every periodic sweep, walked the whole dictionary to
find the few that had expired. That is what forced the cap to be sized for
the scan rather than for the flood.

A hold is now never refreshed: the first detection sets the expiry and
later ones leave it alone. That makes insertion order equal to expiry order,
so a ring of the same keys is sorted by construction and retiring lapsed
entries stops at the first live record -- the cost is the number expiring,
not the number held. Nothing is lost by dropping the refresh: the rate
limiter runs ahead of the connection filters (NetState.Network.cs) and
reports to the ban channel, so a flooder whose hold lapses is re-held on its
next attempt.

Because the ring carries the expiry, the dictionary only had to answer
membership, so it is a HashSet now: 36 bytes a slot against 52. The ring is
parallel UInt128[]/long[] rather than an array of structs -- UInt128 forces
16-byte alignment, so a packed pair would cost 32 bytes where these cost 24,
and the drain reads only the long[].

Measured, standalone copies of both designs head to head at the shipped
324,449 cap:

  accept path, nothing held      9.1ns -> 6.1ns per call
  sustained flood at cap        26.7ms -> 9.3ms over 60k rejected addresses
  flood end, realistic spread    9.49ms -> 0.05ms worst single call
  flood end, all at one instant  8.85ms -> 10.69ms

The third line is the point: the on-loop stall drops 190x because the work
is spread across the calls that were happening anyway. Two honest costs.
Total work over that spread rises 1.9x (sequential dictionary scan beats
random-access set removes on cache), and the synthetic case where every
entry shares one expiry millisecond is 21% worse. Peak latency is the
currency for a game loop, and reaching that synthetic case needs an entire
flood to land inside one millisecond.

The SweepThrottleMs added earlier goes away with the scan it was throttling.
The periodic timer stays, now O(expiring), purely to reclaim memory on a
shard that goes quiet after a flood.

Release now purges the ring record too. Nothing records that a key was
released, so a re-detection before the old record lapsed would otherwise be
retired early by it. O(n), on an operator retraction.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 22:13:31 -07:00
..
Application fix: Bumps dependencies. (#2531) 2026-07-14 15:17:55 -07:00
BuildTool fix: Require only runtime packages on Linux, and check ICU and tzdata the way the runtime does (#2561) 2026-08-07 15:03:08 -07:00
Logger fix: Bumps dependencies. (#2531) 2026-07-14 15:17:55 -07:00
Server fix: stop the idle-sleep backoff tripping on healthy hosts (#2572) 2026-08-13 19:58:03 -07:00
Server.Tests fix: stop stack merges and splits from laundering PlayerConstructed (#2576) 2026-08-13 19:18:06 -07:00
UOContent perf: retire auto-denylist holds from an expiry ring instead of scanning 2026-08-13 22:13:31 -07:00
UOContent.Tests perf: retire auto-denylist holds from an expiry ring instead of scanning 2026-08-13 22:13:31 -07:00