perf(net): frame recurring sends in place

Write normal game-message and ACK packet framing directly into bounded stack spans, hash fragments without materialization, and send the populated slice to the socket. Preserve exact wire bytes and ISAAC failure ordering with differential and zero-allocation tests.
This commit is contained in:
Erik 2026-07-25 06:02:52 +02:00
parent e928c5dd02
commit 41c1a59392
7 changed files with 485 additions and 31 deletions

View file

@ -54,6 +54,50 @@ public static class GameMessageFragment
return new MessageFragment(header, gameMessageBytes.ToArray());
}
/// <summary>
/// Write one complete fragment directly into caller-owned storage.
/// Returns the number of bytes written. This is the production send-path
/// primitive; it creates no intermediate payload or serialized array.
/// </summary>
internal static int WriteSingleFragment(
Span<byte> destination,
uint fragmentSequence,
GameMessageGroup queue,
ReadOnlySpan<byte> gameMessageBytes)
{
if (gameMessageBytes.Length
> MessageFragmentHeader.MaxFragmentDataSize)
{
throw new ArgumentException(
$"game message body ({gameMessageBytes.Length} bytes) exceeds single-fragment capacity "
+ $"({MessageFragmentHeader.MaxFragmentDataSize} bytes). Multi-fragment split TBD.",
nameof(gameMessageBytes));
}
int wireSize =
MessageFragmentHeader.Size + gameMessageBytes.Length;
if (destination.Length < wireSize)
{
throw new ArgumentException(
$"destination must be at least {wireSize} bytes",
nameof(destination));
}
var header = new MessageFragmentHeader
{
Sequence = fragmentSequence,
Id = OutboundFragmentId,
Count = 1,
TotalSize = checked((ushort)wireSize),
Index = 0,
Queue = (ushort)queue,
};
header.Pack(destination);
gameMessageBytes.CopyTo(
destination.Slice(MessageFragmentHeader.Size));
return wireSize;
}
/// <summary>
/// Concatenate a fragment's header + payload into the bytes that go
/// into a packet's body. Use when building the full <c>body</c> span

View file

@ -520,8 +520,6 @@ public static class PacketCodec
/// </param>
public static byte[] Encode(PacketHeader header, ReadOnlySpan<byte> body, IsaacRandom? outboundIsaac)
{
header.DataSize = checked((ushort)body.Length);
// Parse the optional-section length out of the body so we can hash
// it separately from any subsequent fragments. Without the BlobFragments
// flag, the entire body IS the optional section. With BlobFragments,
@ -541,43 +539,129 @@ public static class PacketCodec
if (optionalLen < 0)
throw new ArgumentException("body's optional section is malformed", nameof(body));
uint optionalHash = Hash32.Calculate(body.Slice(0, optionalLen));
byte[] datagram = new byte[PacketHeader.Size + body.Length];
body.CopyTo(datagram.AsSpan(PacketHeader.Size));
FinalizeInPlace(
header,
datagram,
body.Length,
optionalLen,
outboundIsaac);
return datagram;
}
// Hash any fragments in the body tail.
uint fragmentHash = 0;
if (header.HasFlag(PacketHeaderFlags.BlobFragments))
/// <summary>
/// Finalize a packet whose body has already been written immediately
/// after the fixed header in <paramref name="datagram"/>. Computes the
/// exact optional/fragment hash, consumes the outbound ISAAC word when
/// required, and writes the fixed header in place.
/// </summary>
internal static int FinalizeInPlace(
PacketHeader header,
Span<byte> datagram,
int bodyLength,
int optionalLength,
IsaacRandom? outboundIsaac)
{
if ((uint)bodyLength > ushort.MaxValue)
{
var tail = body.Slice(optionalLen);
while (tail.Length > 0)
{
var (frag, consumed) = MessageFragment.TryParse(tail);
if (frag is null || consumed == 0)
throw new ArgumentException("body contains a malformed fragment", nameof(body));
fragmentHash += CalculateFragmentHash32(frag.Value);
tail = tail.Slice(consumed);
}
throw new ArgumentOutOfRangeException(
nameof(bodyLength));
}
uint headerHash = header.CalculateHeaderHash32();
uint payloadHash = optionalHash + fragmentHash;
int datagramLength = checked(PacketHeader.Size + bodyLength);
if (datagram.Length < datagramLength)
{
throw new ArgumentException(
$"datagram must be at least {datagramLength} bytes",
nameof(datagram));
}
if ((uint)optionalLength > (uint)bodyLength)
{
throw new ArgumentOutOfRangeException(
nameof(optionalLength));
}
ReadOnlySpan<byte> body = datagram.Slice(
PacketHeader.Size,
bodyLength);
uint payloadHash = CalculatePayloadHash(
body,
header.Flags,
optionalLength);
header.DataSize = checked((ushort)bodyLength);
uint headerHash = header.CalculateHeaderHash32();
if (header.HasFlag(PacketHeaderFlags.EncryptedChecksum))
{
if (outboundIsaac is null)
{
throw new InvalidOperationException(
"EncryptedChecksum flag set but no ISAAC keystream provided");
}
uint isaacKey = outboundIsaac.Next();
header.Checksum = headerHash + (isaacKey ^ payloadHash);
header.Checksum =
headerHash + (isaacKey ^ payloadHash);
}
else
{
header.Checksum = headerHash + payloadHash;
}
byte[] datagram = new byte[PacketHeader.Size + body.Length];
header.Pack(datagram);
body.CopyTo(datagram.AsSpan(PacketHeader.Size));
return datagram;
return datagramLength;
}
private static uint CalculatePayloadHash(
ReadOnlySpan<byte> body,
PacketHeaderFlags flags,
int optionalLength)
{
uint optionalHash = Hash32.Calculate(
body.Slice(0, optionalLength));
if ((flags & PacketHeaderFlags.BlobFragments) == 0)
{
if (optionalLength != body.Length)
{
throw new ArgumentException(
"non-fragment body contains bytes outside the optional section",
nameof(body));
}
return optionalHash;
}
uint fragmentHash = 0;
ReadOnlySpan<byte> remaining =
body.Slice(optionalLength);
while (!remaining.IsEmpty)
{
if (!MessageFragment.TryParseLayout(
remaining,
out _,
out int payloadLength,
out int consumed))
{
throw new ArgumentException(
"body contains a malformed fragment",
nameof(body));
}
fragmentHash +=
Hash32.Calculate(
remaining.Slice(
0,
MessageFragmentHeader.Size))
+ Hash32.Calculate(
remaining.Slice(
MessageFragmentHeader.Size,
payloadLength));
remaining = remaining.Slice(consumed);
}
return optionalHash + fragmentHash;
}
/// <summary>

View file

@ -2152,17 +2152,28 @@ public sealed class WorldSession : IDisposable
private void SendGameMessage(byte[] gameMessageBody, GameMessageGroup queue)
{
var fragment = GameMessageFragment.BuildSingleFragment(
_fragmentSequence++, queue, gameMessageBody);
byte[] packetBody = GameMessageFragment.Serialize(fragment);
Span<byte> datagram = stackalloc byte[
PacketHeader.Size
+ MessageFragmentHeader.MaxFragmentSize];
int fragmentLength =
GameMessageFragment.WriteSingleFragment(
datagram.Slice(PacketHeader.Size),
_fragmentSequence++,
queue,
gameMessageBody);
var header = new PacketHeader
{
Sequence = _clientPacketSequence++,
Flags = PacketHeaderFlags.BlobFragments | PacketHeaderFlags.EncryptedChecksum,
Id = _sessionClientId,
};
byte[] datagram = PacketCodec.Encode(header, packetBody, _outboundIsaac);
_net.Send(datagram);
int datagramLength = PacketCodec.FinalizeInPlace(
header,
datagram,
fragmentLength,
optionalLength: 0,
_outboundIsaac);
_net.Send(datagram.Slice(0, datagramLength));
}
/// <summary>
@ -2191,8 +2202,11 @@ public sealed class WorldSession : IDisposable
private void SendAck(uint serverPacketSequence)
{
// 4-byte body: little-endian u32 of the server sequence we're acking.
Span<byte> body = stackalloc byte[4];
BinaryPrimitives.WriteUInt32LittleEndian(body, serverPacketSequence);
Span<byte> datagram = stackalloc byte[
PacketHeader.Size + sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(
datagram.Slice(PacketHeader.Size),
serverPacketSequence);
// Holtburger uses current_client_sequence (= packet_sequence - 1) for
// ack headers. We mirror that — acks borrow the most recently issued
@ -2208,8 +2222,13 @@ public sealed class WorldSession : IDisposable
Id = _sessionClientId,
};
byte[] datagram = PacketCodec.Encode(header, body, outboundIsaac: null);
_net.Send(datagram);
int datagramLength = PacketCodec.FinalizeInPlace(
header,
datagram,
bodyLength: sizeof(uint),
optionalLength: sizeof(uint),
outboundIsaac: null);
_net.Send(datagram.Slice(0, datagramLength));
}
private void Transition(State next)