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.
This commit is contained in:
parent
7211bb1bf7
commit
e928c5dd02
18 changed files with 1329 additions and 66 deletions
|
|
@ -35,16 +35,16 @@ public static class EmoteText
|
|||
string SenderName,
|
||||
string Text);
|
||||
|
||||
public static Parsed? TryParse(byte[] body)
|
||||
public static Parsed? TryParse(ReadOnlySpan<byte> body)
|
||||
{
|
||||
if (body is null || body.Length < 8) return null;
|
||||
if (body.Length < 8) return null;
|
||||
uint opcode = BinaryPrimitives.ReadUInt32LittleEndian(body);
|
||||
if (opcode != Opcode) return null;
|
||||
|
||||
try
|
||||
{
|
||||
int pos = 4;
|
||||
uint senderGuid = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(pos));
|
||||
uint senderGuid = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
string senderName = StringReader.ReadString16L(body, ref pos);
|
||||
string text = StringReader.ReadString16L(body, ref pos);
|
||||
|
|
|
|||
|
|
@ -17,7 +17,10 @@ namespace AcDream.Core.Net.Messages;
|
|||
/// Handlers are invoked synchronously on the thread that called
|
||||
/// <see cref="Dispatch"/> — normally the render thread since
|
||||
/// <see cref="WorldSession"/>'s decode path runs there in the current
|
||||
/// architecture. Handlers must not block.
|
||||
/// architecture. Handlers must not block or retain the envelope/payload:
|
||||
/// production payload memory borrows a pooled inbound datagram and expires
|
||||
/// when the synchronous dispatch call returns. Handlers must parse or copy
|
||||
/// any state they need to keep.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class GameEventDispatcher
|
||||
|
|
@ -86,7 +89,8 @@ public sealed class GameEventDispatcher
|
|||
/// <summary>
|
||||
/// Route an envelope to its handler, or log as unhandled. Exceptions
|
||||
/// inside handlers are swallowed to keep the decode loop alive — a
|
||||
/// malformed event from the server should not crash the client.
|
||||
/// malformed event from the server should not crash the client. The
|
||||
/// handler must not retain <paramref name="envelope"/> or its payload.
|
||||
/// </summary>
|
||||
public void Dispatch(GameEventEnvelope envelope)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -42,16 +42,28 @@ public readonly record struct GameEventEnvelope(
|
|||
/// </summary>
|
||||
public static GameEventEnvelope? TryParse(byte[] body)
|
||||
{
|
||||
if (body is null || body.Length < HeaderSize) return null;
|
||||
ArgumentNullException.ThrowIfNull(body);
|
||||
return TryParseBorrowed(body);
|
||||
}
|
||||
|
||||
uint outer = BinaryPrimitives.ReadUInt32LittleEndian(body);
|
||||
/// <summary>
|
||||
/// Internal receive-path parser. The returned payload borrows
|
||||
/// <paramref name="body"/> and must not escape synchronous dispatch.
|
||||
/// </summary>
|
||||
internal static GameEventEnvelope? TryParseBorrowed(
|
||||
ReadOnlyMemory<byte> body)
|
||||
{
|
||||
if (body.Length < HeaderSize) return null;
|
||||
|
||||
ReadOnlySpan<byte> span = body.Span;
|
||||
uint outer = BinaryPrimitives.ReadUInt32LittleEndian(span);
|
||||
if (outer != Opcode) return null;
|
||||
|
||||
uint guid = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(4));
|
||||
uint sequence = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8));
|
||||
uint eventType = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12));
|
||||
uint guid = BinaryPrimitives.ReadUInt32LittleEndian(span.Slice(4));
|
||||
uint sequence = BinaryPrimitives.ReadUInt32LittleEndian(span.Slice(8));
|
||||
uint eventType = BinaryPrimitives.ReadUInt32LittleEndian(span.Slice(12));
|
||||
|
||||
var payload = body.AsMemory(HeaderSize);
|
||||
ReadOnlyMemory<byte> payload = body.Slice(HeaderSize);
|
||||
return new GameEventEnvelope(guid, sequence, (GameEventType)eventType, payload);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,9 +46,9 @@ public static class HearSpeech
|
|||
uint ChatType,
|
||||
bool IsRanged);
|
||||
|
||||
public static Parsed? TryParse(byte[] body)
|
||||
public static Parsed? TryParse(ReadOnlySpan<byte> body)
|
||||
{
|
||||
if (body is null || body.Length < 16) return null;
|
||||
if (body.Length < 16) return null;
|
||||
|
||||
uint opcode = BinaryPrimitives.ReadUInt32LittleEndian(body);
|
||||
bool isRanged;
|
||||
|
|
@ -62,8 +62,8 @@ public static class HearSpeech
|
|||
string text = ReadString16L(body, ref pos);
|
||||
string sender = ReadString16L(body, ref pos);
|
||||
if (body.Length - pos < 8) return null;
|
||||
uint senderGuid = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(pos)); pos += 4;
|
||||
uint chatType = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(pos)); pos += 4;
|
||||
uint senderGuid = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos)); pos += 4;
|
||||
uint chatType = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos)); pos += 4;
|
||||
return new Parsed(text, sender, senderGuid, chatType, isRanged);
|
||||
}
|
||||
catch { return null; }
|
||||
|
|
|
|||
|
|
@ -30,9 +30,9 @@ public static class PlayerKilled
|
|||
uint VictimGuid,
|
||||
uint KillerGuid);
|
||||
|
||||
public static Parsed? TryParse(byte[] body)
|
||||
public static Parsed? TryParse(ReadOnlySpan<byte> body)
|
||||
{
|
||||
if (body is null || body.Length < 4) return null;
|
||||
if (body.Length < 4) return null;
|
||||
uint opcode = BinaryPrimitives.ReadUInt32LittleEndian(body);
|
||||
if (opcode != Opcode) return null;
|
||||
|
||||
|
|
@ -41,9 +41,9 @@ public static class PlayerKilled
|
|||
int pos = 4;
|
||||
string deathMessage = StringReader.ReadString16L(body, ref pos);
|
||||
if (body.Length - pos < 8) return null;
|
||||
uint victimGuid = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(pos));
|
||||
uint victimGuid = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
uint killerGuid = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(pos));
|
||||
uint killerGuid = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos));
|
||||
return new Parsed(deathMessage, victimGuid, killerGuid);
|
||||
}
|
||||
catch { return null; }
|
||||
|
|
|
|||
|
|
@ -32,9 +32,9 @@ public static class ServerMessage
|
|||
|
||||
public readonly record struct Parsed(string Message, uint ChatType);
|
||||
|
||||
public static Parsed? TryParse(byte[] body)
|
||||
public static Parsed? TryParse(ReadOnlySpan<byte> body)
|
||||
{
|
||||
if (body is null || body.Length < 8) return null;
|
||||
if (body.Length < 8) return null;
|
||||
uint opcode = BinaryPrimitives.ReadUInt32LittleEndian(body);
|
||||
if (opcode != Opcode) return null;
|
||||
|
||||
|
|
@ -43,7 +43,7 @@ public static class ServerMessage
|
|||
int pos = 4;
|
||||
string message = StringReader.ReadString16L(body, ref pos);
|
||||
if (body.Length - pos < 4) return null;
|
||||
uint chatType = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(pos));
|
||||
uint chatType = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos));
|
||||
return new Parsed(message, chatType);
|
||||
}
|
||||
catch { return null; }
|
||||
|
|
|
|||
|
|
@ -31,16 +31,16 @@ public static class SoulEmote
|
|||
string SenderName,
|
||||
string Text);
|
||||
|
||||
public static Parsed? TryParse(byte[] body)
|
||||
public static Parsed? TryParse(ReadOnlySpan<byte> body)
|
||||
{
|
||||
if (body is null || body.Length < 8) return null;
|
||||
if (body.Length < 8) return null;
|
||||
uint opcode = BinaryPrimitives.ReadUInt32LittleEndian(body);
|
||||
if (opcode != Opcode) return null;
|
||||
|
||||
try
|
||||
{
|
||||
int pos = 4;
|
||||
uint senderGuid = BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(pos));
|
||||
uint senderGuid = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(pos));
|
||||
pos += 4;
|
||||
string senderName = StringReader.ReadString16L(body, ref pos);
|
||||
string text = StringReader.ReadString16L(body, ref pos);
|
||||
|
|
|
|||
|
|
@ -143,9 +143,9 @@ public static class TurbineChat
|
|||
/// for any structural error (truncation, unknown blob_type, ACE
|
||||
/// id-mismatch in RequestSendToRoomById, malformed UTF-16 string).
|
||||
/// </summary>
|
||||
public static Parsed? TryParse(byte[] body)
|
||||
public static Parsed? TryParse(ReadOnlySpan<byte> body)
|
||||
{
|
||||
if (body is null || body.Length < 4 + HeaderSize) return null;
|
||||
if (body.Length < 4 + HeaderSize) return null;
|
||||
uint opcode = BinaryPrimitives.ReadUInt32LittleEndian(body);
|
||||
if (opcode != Opcode) return null;
|
||||
|
||||
|
|
@ -259,7 +259,7 @@ public static class TurbineChat
|
|||
int remaining = expectedPayload;
|
||||
if (body.Length - pos < remaining) return null;
|
||||
var bytes = new byte[remaining];
|
||||
Array.Copy(body, pos, bytes, 0, remaining);
|
||||
body.Slice(pos, remaining).CopyTo(bytes);
|
||||
pos += remaining;
|
||||
payload = new Payload.Unknown(bytes);
|
||||
break;
|
||||
|
|
@ -403,7 +403,9 @@ public static class TurbineChat
|
|||
/// the high bit of the first byte is set the prefix is two bytes
|
||||
/// (high 7 bits of byte 0 + all 8 bits of byte 1).
|
||||
/// </summary>
|
||||
public static string ReadTurbineString(byte[] data, ref int pos)
|
||||
public static string ReadTurbineString(
|
||||
ReadOnlySpan<byte> data,
|
||||
ref int pos)
|
||||
{
|
||||
if (data.Length - pos < 1) throw new FormatException("turbine str: truncated len");
|
||||
int chars = data[pos];
|
||||
|
|
@ -422,7 +424,8 @@ public static class TurbineChat
|
|||
|
||||
// String.from_utf16 in Rust validates surrogate pairs; .NET's
|
||||
// Encoding.Unicode.GetString matches that semantics.
|
||||
string s = Encoding.Unicode.GetString(data, pos, (int)bytesLen);
|
||||
string s = Encoding.Unicode.GetString(
|
||||
data.Slice(pos, (int)bytesLen));
|
||||
pos += (int)bytesLen;
|
||||
return s;
|
||||
}
|
||||
|
|
@ -463,9 +466,12 @@ public static class TurbineChat
|
|||
// Helpers
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
private static uint ReadU32(byte[] data, ref int pos)
|
||||
private static uint ReadU32(
|
||||
ReadOnlySpan<byte> data,
|
||||
ref int pos)
|
||||
{
|
||||
uint v = BinaryPrimitives.ReadUInt32LittleEndian(data.AsSpan(pos, 4));
|
||||
uint v = BinaryPrimitives.ReadUInt32LittleEndian(
|
||||
data.Slice(pos, 4));
|
||||
pos += 4;
|
||||
return v;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue