perf: size the auto-denylist cap to a Dictionary capacity

65,536 sat just past a resize boundary. Dictionary<UInt128,long> grown from
empty steps 36,353 -> 75,431 -> 156,437 (ExpandPrime doubles and takes the
next prime), so a 65,536 cap allocated capacity 75,431 and stranded 9,895
slots at 52 bytes each -- 515 KB reserved and unreachable.

156,437 fills a capacity exactly: 2.4x the headroom for ~8 MB, no wasted
slots. Measured Entry layout is 48 bytes (4 hashCode + 4 next + 8 alignment
padding for UInt128 + 16 key + 8 value) plus a 4-byte bucket int.

The binding constraint is the sweep, not memory, since it is O(n) on the
loop. Measured full sweeps: ~2 ms at 156,437, ~6 ms at 324,449, ~26 ms at
968,897 -- so a million-entry cap costs 72 MB and a three-tick stall when a
flood's entries all lapse together. The remark records the curve because
maxEntries is operator-facing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kamron Batman 2026-08-13 21:54:44 -07:00
parent a5c785c4a0
commit ff7e70258e
No known key found for this signature in database
GPG key ID: 7D81DF26D9A5D94A

View file

@ -75,6 +75,12 @@ public record AutoDenylistSettings
/// stops it becoming the exhaustion it prevents. At the cap new addresses are not tracked, but are still
/// disconnected by whichever gate detected them.
/// </summary>
/// <remarks>
/// A <c>Dictionary</c> capacity, not a round number. Growing from empty it steps 36,353 → 75,431 →
/// 156,437, so this fills one exactly instead of stranding slots: 65,536 sat just past a resize and left
/// 9,895 of them (52 bytes each) unusable. Costs ~8 MB. Raising it is paid on the loop, where a full
/// sweep runs ~2 ms here, ~6 ms at 324,449 and ~26 ms at 968,897.
/// </remarks>
[JsonPropertyName("maxEntries")]
public int MaxEntries { get; set; } = 65536;
public int MaxEntries { get; set; } = 156_437;
}