acdream/tests/AcDream.Core.Net.Tests/Packets/BorrowedPacketCodecTests.cs
Erik e928c5dd02 perf(net): borrow inbound packet storage
Decode headers, optional fields, fragments, and single-fragment messages directly over pooled datagrams. Copy only fragment state that crosses a datagram lifetime, preserve synchronous dispatch and ACK ordering, and lock the path to the owned decoder with differential and zero-allocation tests.
2026-07-25 05:58:55 +02:00

441 lines
14 KiB
C#

using System.Buffers.Binary;
using AcDream.Core.Net.Cryptography;
using AcDream.Core.Net.Packets;
namespace AcDream.Core.Net.Tests.Packets;
public class BorrowedPacketCodecTests
{
[Fact]
public void TryDecodeBorrowed_AllOptionalFieldsAndFragments_MatchesOwnedDecoder()
{
const PacketHeaderFlags flags =
PacketHeaderFlags.ServerSwitch
| PacketHeaderFlags.RequestRetransmit
| PacketHeaderFlags.RejectRetransmit
| PacketHeaderFlags.AckSequence
| PacketHeaderFlags.WorldLoginRequest
| PacketHeaderFlags.ConnectRequest
| PacketHeaderFlags.ConnectResponse
| PacketHeaderFlags.CICMDCommand
| PacketHeaderFlags.TimeSync
| PacketHeaderFlags.EchoRequest
| PacketHeaderFlags.Flow
| PacketHeaderFlags.BlobFragments;
var optional = new byte[8 + 12 + 8 + 4 + 8 + 32 + 8 + 8 + 8 + 4 + 6];
int position = 0;
Fill(optional, ref position, 8, 0x11);
WriteUInt32(optional, ref position, 2);
WriteUInt32(optional, ref position, 0x10111213);
WriteUInt32(optional, ref position, 0x20212223);
WriteUInt32(optional, ref position, 1);
WriteUInt32(optional, ref position, 0x30313233);
WriteUInt32(optional, ref position, 0x40414243);
Fill(optional, ref position, 8, 0x44);
double serverTime = 12345.6789;
ulong cookie = 0xFEEDFACECAFEBABE;
uint clientId = 0x50515253;
uint serverSeed = 0x60616263;
uint clientSeed = 0x70717273;
WriteDouble(optional, ref position, serverTime);
WriteUInt64(optional, ref position, cookie);
WriteUInt32(optional, ref position, clientId);
WriteUInt32(optional, ref position, serverSeed);
WriteUInt32(optional, ref position, clientSeed);
WriteUInt32(optional, ref position, 0);
Fill(optional, ref position, 8, 0x81);
Fill(optional, ref position, 8, 0x91);
double timeSync = 9876.54321;
float echoTime = 42.25f;
WriteDouble(optional, ref position, timeSync);
WriteSingle(optional, ref position, echoTime);
WriteUInt32(optional, ref position, 0xA0A1A2A3);
WriteUInt16(optional, ref position, 0xB0B1);
Assert.Equal(optional.Length, position);
byte[] first = BuildFragment(
sequence: 99,
count: 2,
index: 0,
queue: 7,
payload: [0xC1, 0xC2]);
byte[] second = BuildFragment(
sequence: 99,
count: 2,
index: 1,
queue: 7,
payload: [0xD1, 0xD2, 0xD3]);
byte[] body = [.. optional, .. first, .. second];
byte[] datagram = Encode(flags, body);
PacketCodec.PacketDecodeResult owned =
PacketCodec.TryDecode(datagram, inboundIsaac: null);
BorrowedPacketDecodeResult borrowed =
PacketCodec.TryDecodeBorrowed(datagram, inboundIsaac: null);
AssertEquivalent(owned, borrowed);
Assert.Equal(serverTime, borrowed.Packet.Optional.ConnectRequestServerTime);
Assert.Equal(cookie, borrowed.Packet.Optional.ConnectRequestCookie);
Assert.Equal(clientId, borrowed.Packet.Optional.ConnectRequestClientId);
Assert.Equal(serverSeed, borrowed.Packet.Optional.ConnectRequestServerSeed);
Assert.Equal(clientSeed, borrowed.Packet.Optional.ConnectRequestClientSeed);
Assert.Equal(timeSync, borrowed.Packet.Optional.TimeSync);
Assert.Equal(echoTime, borrowed.Packet.Optional.EchoRequestClientTime);
Assert.Equal(0xA0A1A2A3u, borrowed.Packet.Optional.FlowBytes);
Assert.Equal(0xB0B1, borrowed.Packet.Optional.FlowInterval);
Assert.Equal(
new uint[] { 0x10111213, 0x20212223 },
ReadRetransmits(borrowed.Packet.Optional));
}
[Fact]
public void TryDecodeBorrowed_LoginPayload_MatchesOwnedDecoder()
{
byte[] payload = LoginRequest.Build("borrowed", "packet", 47);
byte[] datagram = Encode(PacketHeaderFlags.LoginRequest, payload);
PacketCodec.PacketDecodeResult owned =
PacketCodec.TryDecode(datagram, inboundIsaac: null);
BorrowedPacketDecodeResult borrowed =
PacketCodec.TryDecodeBorrowed(datagram, inboundIsaac: null);
AssertEquivalent(owned, borrowed);
Assert.Equal(payload, borrowed.Packet.Optional.RawBytes.ToArray());
Assert.Equal(payload, borrowed.Packet.Body.ToArray());
Assert.Equal(0, borrowed.Packet.FragmentCount);
}
[Fact]
public void TryDecodeBorrowed_EncryptedChecksum_MatchesOwnedDecoder()
{
byte[] body = new byte[4];
BinaryPrimitives.WriteUInt32LittleEndian(body, 0x12345678);
byte[] seed = [0x44, 0x33, 0x22, 0x11];
byte[] datagram = Encode(
PacketHeaderFlags.AckSequence
| PacketHeaderFlags.EncryptedChecksum,
body,
new IsaacRandom(seed));
PacketCodec.PacketDecodeResult owned =
PacketCodec.TryDecode(
datagram,
new IsaacRandom(seed));
BorrowedPacketDecodeResult borrowed =
PacketCodec.TryDecodeBorrowed(
datagram,
new IsaacRandom(seed));
AssertEquivalent(owned, borrowed);
}
[Fact]
public void TryDecodeBorrowed_MalformedPackets_MatchOwnedDecoderErrors()
{
byte[] shortHeader = new byte[PacketHeader.Size - 1];
AssertSameError(shortHeader);
var oversized = new byte[PacketHeader.Size + 2];
new PacketHeader { DataSize = 3 }.Pack(oversized);
AssertSameError(oversized);
byte[] shortOptional = EncodeUnchecked(
PacketHeaderFlags.TimeSync,
[0x01, 0x02, 0x03, 0x04]);
AssertSameError(shortOptional);
byte[] zeroCount = BuildFragment(
sequence: 1,
count: 0,
index: 0,
queue: 0,
payload: [0x01]);
AssertSameError(EncodeUnchecked(
PacketHeaderFlags.BlobFragments,
zeroCount));
byte[] invalidIndex = BuildFragment(
sequence: 1,
count: 1,
index: 1,
queue: 0,
payload: [0x01]);
AssertSameError(EncodeUnchecked(
PacketHeaderFlags.BlobFragments,
invalidIndex));
byte[] wrongChecksum = Encode(
PacketHeaderFlags.AckSequence,
[1, 2, 3, 4]);
wrongChecksum[8] ^= 0x80;
AssertSameError(wrongChecksum);
}
[Fact]
public void TryDecodeBorrowed_WarmSingleFragmentPath_AllocatesNothing()
{
byte[] fragment = BuildFragment(
sequence: 77,
count: 1,
index: 0,
queue: 5,
payload: [0xAA, 0xBB, 0xCC, 0xDD]);
byte[] datagram = Encode(
PacketHeaderFlags.BlobFragments,
fragment);
int checksum = DecodeAndRead(datagram);
long before = GC.GetAllocatedBytesForCurrentThread();
for (int iteration = 0; iteration < 1_000; iteration++)
checksum += DecodeAndRead(datagram);
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
Assert.NotEqual(0, checksum);
Assert.Equal(0, allocated);
}
private static int DecodeAndRead(ReadOnlyMemory<byte> datagram)
{
BorrowedPacketDecodeResult decoded =
PacketCodec.TryDecodeBorrowed(
datagram,
inboundIsaac: null);
int checksum = decoded.Packet.FragmentCount;
foreach (BorrowedMessageFragment fragment
in decoded.Packet.Fragments)
{
checksum += fragment.Header.TotalSize;
checksum += fragment.Payload.Span[0];
}
return checksum;
}
private static void AssertEquivalent(
PacketCodec.PacketDecodeResult owned,
BorrowedPacketDecodeResult borrowed)
{
Assert.Equal(owned.Error, borrowed.Error);
if (!owned.IsOk)
return;
Packet packet = Assert.IsType<Packet>(owned.Packet);
Assert.Equal(packet.Header, borrowed.Packet.Header);
Assert.Equal(packet.BodyBytes, borrowed.Packet.Body.ToArray());
Assert.Equal(
packet.Optional.RawBytes,
borrowed.Packet.Optional.RawBytes.ToArray());
Assert.Equal(
packet.Optional.AckSequence,
borrowed.Packet.Optional.AckSequence);
Assert.Equal(
packet.Optional.TimeSync,
borrowed.Packet.Optional.TimeSync);
Assert.Equal(
packet.Optional.EchoRequestClientTime,
borrowed.Packet.Optional.EchoRequestClientTime);
Assert.Equal(
packet.Optional.FlowBytes,
borrowed.Packet.Optional.FlowBytes);
Assert.Equal(
packet.Optional.FlowInterval,
borrowed.Packet.Optional.FlowInterval);
Assert.Equal(
packet.Optional.ConnectRequestServerTime,
borrowed.Packet.Optional.ConnectRequestServerTime);
Assert.Equal(
packet.Optional.ConnectRequestCookie,
borrowed.Packet.Optional.ConnectRequestCookie);
Assert.Equal(
packet.Optional.ConnectRequestClientId,
borrowed.Packet.Optional.ConnectRequestClientId);
Assert.Equal(
packet.Optional.ConnectRequestServerSeed,
borrowed.Packet.Optional.ConnectRequestServerSeed);
Assert.Equal(
packet.Optional.ConnectRequestClientSeed,
borrowed.Packet.Optional.ConnectRequestClientSeed);
Assert.Equal(
packet.Optional.RetransmitRequests,
ReadRetransmits(borrowed.Packet.Optional));
var fragments = new List<BorrowedMessageFragment>();
foreach (BorrowedMessageFragment fragment
in borrowed.Packet.Fragments)
{
fragments.Add(fragment);
}
Assert.Equal(packet.Fragments.Count, borrowed.Packet.FragmentCount);
Assert.Equal(packet.Fragments.Count, fragments.Count);
for (int index = 0; index < fragments.Count; index++)
{
Assert.Equal(
packet.Fragments[index].Header,
fragments[index].Header);
Assert.Equal(
packet.Fragments[index].Payload,
fragments[index].Payload.ToArray());
}
}
private static uint[] ReadRetransmits(
BorrowedOptionalHeader optional)
{
var retransmits = new uint[optional.RetransmitRequestCount];
ReadOnlySpan<byte> bytes = optional.RetransmitRequestBytes.Span;
for (int index = 0; index < retransmits.Length; index++)
{
retransmits[index] =
BinaryPrimitives.ReadUInt32LittleEndian(
bytes.Slice(index * 4));
}
return retransmits;
}
private static void AssertSameError(byte[] datagram)
{
PacketCodec.PacketDecodeResult owned =
PacketCodec.TryDecode(datagram, inboundIsaac: null);
BorrowedPacketDecodeResult borrowed =
PacketCodec.TryDecodeBorrowed(
datagram,
inboundIsaac: null);
Assert.Equal(owned.Error, borrowed.Error);
}
private static byte[] Encode(
PacketHeaderFlags flags,
byte[] body,
IsaacRandom? isaac = null) =>
PacketCodec.Encode(
new PacketHeader
{
Sequence = 0x01020304,
Flags = flags,
Id = 0x0506,
Time = 0x0708,
Iteration = 0x090A,
},
body,
isaac);
private static byte[] EncodeUnchecked(
PacketHeaderFlags flags,
byte[] body)
{
var header = new PacketHeader
{
Sequence = 0x01020304,
Flags = flags,
Id = 0x0506,
Time = 0x0708,
Iteration = 0x090A,
DataSize = checked((ushort)body.Length),
};
header.Checksum =
header.CalculateHeaderHash32()
+ Hash32.Calculate(body);
byte[] datagram = new byte[PacketHeader.Size + body.Length];
header.Pack(datagram);
body.CopyTo(datagram, PacketHeader.Size);
return datagram;
}
private static byte[] BuildFragment(
uint sequence,
ushort count,
ushort index,
ushort queue,
byte[] payload)
{
var header = new MessageFragmentHeader
{
Sequence = sequence,
Id = 0x80000000,
Count = count,
TotalSize = checked((ushort)(
MessageFragmentHeader.Size + payload.Length)),
Index = index,
Queue = queue,
};
byte[] wire = new byte[header.TotalSize];
header.Pack(wire);
payload.CopyTo(wire, MessageFragmentHeader.Size);
return wire;
}
private static void WriteUInt16(
Span<byte> bytes,
ref int position,
ushort value)
{
BinaryPrimitives.WriteUInt16LittleEndian(
bytes.Slice(position),
value);
position += sizeof(ushort);
}
private static void WriteUInt32(
Span<byte> bytes,
ref int position,
uint value)
{
BinaryPrimitives.WriteUInt32LittleEndian(
bytes.Slice(position),
value);
position += sizeof(uint);
}
private static void WriteUInt64(
Span<byte> bytes,
ref int position,
ulong value)
{
BinaryPrimitives.WriteUInt64LittleEndian(
bytes.Slice(position),
value);
position += sizeof(ulong);
}
private static void WriteSingle(
Span<byte> bytes,
ref int position,
float value)
{
BinaryPrimitives.WriteSingleLittleEndian(
bytes.Slice(position),
value);
position += sizeof(float);
}
private static void WriteDouble(
Span<byte> bytes,
ref int position,
double value)
{
BinaryPrimitives.WriteInt64LittleEndian(
bytes.Slice(position),
BitConverter.DoubleToInt64Bits(value));
position += sizeof(double);
}
private static void Fill(
Span<byte> bytes,
ref int position,
int count,
byte value)
{
bytes.Slice(position, count).Fill(value);
position += count;
}
}