mirror of
https://github.com/modernuo/ModernUO
synced 2026-08-11 22:23:06 -04:00
## Summary Two related fixes on the outbound path: 1. Consume **IORingGroup 1.0.8**, which allows more than one send in flight per socket, and expose the two settings that go with it. 2. Stop `NetState.Send` silently discarding packets when the send buffer fills — including an out-of-bounds write reachable in that state. ## 1. Send-path stall (RIO) RIO reports send completion on **acknowledgement**, not on copy, so a completion cannot arrive sooner than one round trip. With one send in flight, `PostSend` refused to post again until the previous completion arrived — capping a connection at **one send per RTT** whenever it had data queued. Measured on a 50ms-RTT production shard: | | before | after | |---|---|---| | in-game latency, data flowing | **101–146 ms** | **48–51 ms** | | p95 | ~135 ms | 52.8 ms | | samples > 70 ms | 20 | **0** | The control that confirms the mechanism: server-side post→completion was **unchanged** at median 92ms across both runs. The ACK-binding is inherent to RIO and did not move; only its propagation into application latency did. Two things worth recording, because they explain why this went unnoticed: - As little as **6 bytes** of queued data held the gate shut, so it reproduced in empty areas, not just crowded ones. - The same measurement at loopback RTT is **microseconds**, so local testing could never surface it. New settings, both restart-time: - **`network.maxOutstandingSends`** (default 32) — sends in flight per connection. Honoured by RIO only; other backends complete sends on copy and report 1. Costs a request-queue and completion-queue slot per send, **not another buffer**, since every outstanding send addresses a different range of the same registered buffer. Worst-case added latency is roughly `completion RTT / value`. - **`network.sendBufferSize`** (default 256KB) — per-connection send buffer, coerced to a power of two of at least the platform allocation granularity. This is the lever for the disconnects below, and the per-connection memory ceiling. ## 2. Send buffer full `NetState.Send` had three failure modes once the buffer filled, none of them visible: | writable | behaviour | |---|---| | `0` | `GetSendBuffer` returned false → **packet dropped**, no log, no disconnect | | `4 … needed-1` | `Compress` returned 0 → `CommitWrite(0)` → **packet dropped** the same way | | `1 … 3` | `safeOutputLength = (nuint)output.Length - 4` **underflows** → hot-loop bounds check never trips → **writes past the span** | The first two leave a client connected while quietly missing game state, which is undiagnosable from either end. The third corrupts the in-flight region of the ring buffer, and is reachable precisely when a connection is congested, since callers only check for non-zero space. `Compress` now refuses an output too small to bound, and `Send` reports exhaustion instead of dropping — logging and disconnecting with **needed / writable / unacked / capacity**. Those numbers separate a slow client holding the buffer from a buffer genuinely too small for the shard, which is the case that warrants raising `network.sendBufferSize`. ## Testing `NetworkCompressionBoundsTests` covers the underflow using sentinel bytes around the output window. **Verified to fail without the guard** (4 failures from overwritten sentinels), confirming the out-of-bounds writes were real rather than theoretical. Full suites green: **788 Server.Tests**, **597 UOContent.Tests**, Release build clean against the published 1.0.8. ## Notes for reviewers - Upstream change: modernuo/IORingGroup#9. - The buffer-full path is now *loud* where it used to be silent. If a shard has been quietly dropping packets under load, this will surface as disconnects — that is the intended outcome, and the log line says which setting to raise. - Follow-up under discussion: promoting a connection to a larger buffer instead of disconnecting, which looks feasible on a live connection since buffers are referenced per-operation rather than bound to the request queue.
185 lines
8 KiB
C#
185 lines
8 KiB
C#
using System;
|
|
using System.Buffers.Binary;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace Server.Network;
|
|
|
|
/// <summary>
|
|
/// Handles outgoing packet compression for the network.
|
|
/// </summary>
|
|
public static class NetworkCompression
|
|
{
|
|
// UO packets may not exceed 64kb in length
|
|
public const int BufferSize = 0x10000;
|
|
|
|
// Optimal compression ratio is 2 / 8; worst compression ratio is 11 / 8
|
|
private const int MinimalCodeLength = 2;
|
|
|
|
// Fixed overhead, in bits, per compression call
|
|
private const int TerminalCodeLength = 4;
|
|
|
|
// If our input exceeds this length, we cannot possibly compress it within the buffer
|
|
private const int DefiniteOverflow = (BufferSize * 8 - TerminalCodeLength) / MinimalCodeLength;
|
|
|
|
// Packed Table: High 16 bits = Length, Low 16 bits = Value
|
|
private static readonly uint[] _packedHuffmanTable = new uint[257];
|
|
|
|
static NetworkCompression()
|
|
{
|
|
ReadOnlySpan<int> rawTable = [
|
|
0x2, 0x000, 0x5, 0x01F, 0x6, 0x022, 0x7, 0x034, 0x7, 0x075, 0x6, 0x028, 0x6, 0x03B, 0x7, 0x032,
|
|
0x8, 0x0E0, 0x8, 0x062, 0x7, 0x056, 0x8, 0x079, 0x9, 0x19D, 0x8, 0x097, 0x6, 0x02A, 0x7, 0x057,
|
|
0x8, 0x071, 0x8, 0x05B, 0x9, 0x1CC, 0x8, 0x0A7, 0x7, 0x025, 0x7, 0x04F, 0x8, 0x066, 0x8, 0x07D,
|
|
0x9, 0x191, 0x9, 0x1CE, 0x7, 0x03F, 0x9, 0x090, 0x8, 0x059, 0x8, 0x07B, 0x8, 0x091, 0x8, 0x0C6,
|
|
0x6, 0x02D, 0x9, 0x186, 0x8, 0x06F, 0x9, 0x093, 0xA, 0x1CC, 0x8, 0x05A, 0xA, 0x1AE, 0xA, 0x1C0,
|
|
0x9, 0x148, 0x9, 0x14A, 0x9, 0x082, 0xA, 0x19F, 0x9, 0x171, 0x9, 0x120, 0x9, 0x0E7, 0xA, 0x1F3,
|
|
0x9, 0x14B, 0x9, 0x100, 0x9, 0x190, 0x6, 0x013, 0x9, 0x161, 0x9, 0x125, 0x9, 0x133, 0x9, 0x195,
|
|
0x9, 0x173, 0x9, 0x1CA, 0x9, 0x086, 0x9, 0x1E9, 0x9, 0x0DB, 0x9, 0x1EC, 0x9, 0x08B, 0x9, 0x085,
|
|
0x5, 0x00A, 0x8, 0x096, 0x8, 0x09C, 0x9, 0x1C3, 0x9, 0x19C, 0x9, 0x08F, 0x9, 0x18F, 0x9, 0x091,
|
|
0x9, 0x087, 0x9, 0x0C6, 0x9, 0x177, 0x9, 0x089, 0x9, 0x0D6, 0x9, 0x08C, 0x9, 0x1EE, 0x9, 0x1EB,
|
|
0x9, 0x084, 0x9, 0x164, 0x9, 0x175, 0x9, 0x1CD, 0x8, 0x05E, 0x9, 0x088, 0x9, 0x12B, 0x9, 0x172,
|
|
0x9, 0x10A, 0x9, 0x08D, 0x9, 0x13A, 0x9, 0x11C, 0xA, 0x1E1, 0xA, 0x1E0, 0x9, 0x187, 0xA, 0x1DC,
|
|
0xA, 0x1DF, 0x7, 0x074, 0x9, 0x19F, 0x8, 0x08D, 0x8, 0x0E4, 0x7, 0x079, 0x9, 0x0EA, 0x9, 0x0E1,
|
|
0x8, 0x040, 0x7, 0x041, 0x9, 0x10B, 0x9, 0x0B0, 0x8, 0x06A, 0x8, 0x0C1, 0x7, 0x071, 0x7, 0x078,
|
|
0x8, 0x0B1, 0x9, 0x14C, 0x7, 0x043, 0x8, 0x076, 0x7, 0x066, 0x7, 0x04D, 0x9, 0x08A, 0x6, 0x02F,
|
|
0x8, 0x0C9, 0x9, 0x0CE, 0x9, 0x149, 0x9, 0x160, 0xA, 0x1BA, 0xA, 0x19E, 0xA, 0x39F, 0x9, 0x0E5,
|
|
0x9, 0x194, 0x9, 0x184, 0x9, 0x126, 0x7, 0x030, 0x8, 0x06C, 0x9, 0x121, 0x9, 0x1E8, 0xA, 0x1C1,
|
|
0xA, 0x11D, 0xA, 0x163, 0xA, 0x385, 0xA, 0x3DB, 0xA, 0x17D, 0xA, 0x106, 0xA, 0x397, 0xA, 0x24E,
|
|
0x7, 0x02E, 0x8, 0x098, 0xA, 0x33C, 0xA, 0x32E, 0xA, 0x1E9, 0x9, 0x0BF, 0xA, 0x3DF, 0xA, 0x1DD,
|
|
0xA, 0x32D, 0xA, 0x2ED, 0xA, 0x30B, 0xA, 0x107, 0xA, 0x2E8, 0xA, 0x3DE, 0xA, 0x125, 0xA, 0x1E8,
|
|
0x9, 0x0E9, 0xA, 0x1CD, 0xA, 0x1B5, 0x9, 0x165, 0xA, 0x232, 0xA, 0x2E1, 0xB, 0x3AE, 0xB, 0x3C6,
|
|
0xB, 0x3E2, 0xA, 0x205, 0xA, 0x29A, 0xA, 0x248, 0xA, 0x2CD, 0xA, 0x23B, 0xB, 0x3C5, 0xA, 0x251,
|
|
0xA, 0x2E9, 0xA, 0x252, 0x9, 0x1EA, 0xB, 0x3A0, 0xB, 0x391, 0xA, 0x23C, 0xB, 0x392, 0xB, 0x3D5,
|
|
0xA, 0x233, 0xA, 0x2CC, 0xB, 0x390, 0xA, 0x1BB, 0xB, 0x3A1, 0xB, 0x3C4, 0xA, 0x211, 0xA, 0x203,
|
|
0x9, 0x12A, 0xA, 0x231, 0xB, 0x3E0, 0xA, 0x29B, 0xB, 0x3D7, 0xA, 0x202, 0xB, 0x3AD, 0xA, 0x213,
|
|
0xA, 0x253, 0xA, 0x32C, 0xA, 0x23D, 0xA, 0x23F, 0xA, 0x32F, 0xA, 0x11C, 0xA, 0x384, 0xA, 0x31C,
|
|
0xA, 0x17C, 0xA, 0x30A, 0xA, 0x2E0, 0xA, 0x276, 0xA, 0x250, 0xB, 0x3E3, 0xA, 0x396, 0xA, 0x18F,
|
|
0xA, 0x204, 0xA, 0x206, 0xA, 0x230, 0xA, 0x265, 0xA, 0x212, 0xA, 0x23E, 0xB, 0x3AC, 0xB, 0x393,
|
|
0xB, 0x3E1, 0xA, 0x1DE, 0xB, 0x3D6, 0xA, 0x31D, 0xB, 0x3E5, 0xB, 0x3E4, 0xA, 0x207, 0xB, 0x3C7,
|
|
0xA, 0x277, 0xB, 0x3D4, 0x8, 0x0C0, 0xA, 0x162, 0xA, 0x3DA, 0xA, 0x124, 0xA, 0x1B4, 0xA, 0x264,
|
|
0xA, 0x33D, 0xA, 0x1D1, 0xA, 0x1AF, 0xA, 0x39E, 0xA, 0x24F, 0xB, 0x373, 0xA, 0x249, 0xB, 0x372,
|
|
0x9, 0x167, 0xA, 0x210, 0xA, 0x23A, 0xA, 0x1B8, 0xB, 0x3AF, 0xA, 0x18E, 0xA, 0x2EC, 0x7, 0x062,
|
|
0x4, 0x00D
|
|
];
|
|
|
|
for (var i = 0; i < 257; i++)
|
|
{
|
|
var len = (uint)rawTable[i * 2];
|
|
var val = (uint)rawTable[i * 2 + 1];
|
|
_packedHuffmanTable[i] = (len << 16) | val;
|
|
}
|
|
}
|
|
|
|
public static int Compress(ReadOnlySpan<byte> input, Span<byte> output)
|
|
{
|
|
// output.Length < 4 underflows safeOutputLength below (nuint), defeating the hot loop's
|
|
// bounds check. Reachable whenever the send buffer is nearly full.
|
|
if (input.Length > DefiniteOverflow || output.Length < 4)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
ref var inputRef = ref MemoryMarshal.GetReference(input);
|
|
ref var outputRef = ref MemoryMarshal.GetReference(output);
|
|
ref var tableRef = ref MemoryMarshal.GetReference(_packedHuffmanTable.AsSpan());
|
|
|
|
nuint i = 0;
|
|
ulong bitValue = 0;
|
|
var bitCount = 0;
|
|
nuint outputIdx = 0;
|
|
var safeOutputLength = (nuint)output.Length - 4;
|
|
var unrolledLimit = (nuint)input.Length - 1;
|
|
|
|
// Unrolled 2x hot loop using native integer indices
|
|
while (i < unrolledLimit)
|
|
{
|
|
var b1 = Unsafe.Add(ref inputRef, i);
|
|
var entry1 = Unsafe.Add(ref tableRef, b1);
|
|
var len1 = (int)(entry1 >> 16);
|
|
ulong val1 = entry1 & 0xFFFF;
|
|
|
|
bitValue = (bitValue << len1) | val1;
|
|
bitCount += len1;
|
|
|
|
var b2 = Unsafe.Add(ref inputRef, i + 1);
|
|
var entry2 = Unsafe.Add(ref tableRef, b2);
|
|
var len2 = (int)(entry2 >> 16);
|
|
ulong val2 = entry2 & 0xFFFF;
|
|
|
|
bitValue = (bitValue << len2) | val2;
|
|
bitCount += len2;
|
|
|
|
if (bitCount >= 32)
|
|
{
|
|
if (outputIdx > safeOutputLength)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
bitCount -= 32;
|
|
var word = (uint)(bitValue >> bitCount);
|
|
Unsafe.WriteUnaligned(ref Unsafe.Add(ref outputRef, outputIdx), BinaryPrimitives.ReverseEndianness(word));
|
|
outputIdx += 4;
|
|
}
|
|
|
|
i += 2;
|
|
}
|
|
|
|
if (i < (nuint)input.Length)
|
|
{
|
|
var b = Unsafe.Add(ref inputRef, i);
|
|
var entry = Unsafe.Add(ref tableRef, b);
|
|
var len = (int)(entry >> 16);
|
|
ulong val = entry & 0xFFFF;
|
|
|
|
bitValue = (bitValue << len) | val;
|
|
bitCount += len;
|
|
|
|
if (bitCount >= 32)
|
|
{
|
|
if (outputIdx > safeOutputLength)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
bitCount -= 32;
|
|
var word = (uint)(bitValue >> bitCount);
|
|
Unsafe.WriteUnaligned(ref Unsafe.Add(ref outputRef, outputIdx), BinaryPrimitives.ReverseEndianness(word));
|
|
outputIdx += 4;
|
|
}
|
|
}
|
|
|
|
// Terminal code logic
|
|
var termEntry = Unsafe.Add(ref tableRef, 256);
|
|
var termLen = (int)(termEntry >> 16);
|
|
ulong termVal = termEntry & 0xFFFF;
|
|
|
|
bitValue = (bitValue << termLen) | termVal;
|
|
bitCount += termLen;
|
|
|
|
var remainder = bitCount & 7;
|
|
if (remainder != 0)
|
|
{
|
|
var padding = 8 - remainder;
|
|
bitValue <<= padding;
|
|
bitCount += padding;
|
|
}
|
|
|
|
bitValue <<= 64 - bitCount;
|
|
|
|
while (bitCount > 0)
|
|
{
|
|
if (outputIdx >= (nuint)output.Length)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
Unsafe.Add(ref outputRef, outputIdx++) = (byte)(bitValue >> 56);
|
|
bitValue <<= 8;
|
|
bitCount -= 8;
|
|
}
|
|
|
|
return (int)outputIdx;
|
|
}
|
|
}
|