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
|
|
@ -801,8 +801,11 @@ public sealed class WorldSession : IDisposable
|
|||
_net.Send(PacketCodec.Encode(loginHeader, loginPayload, null));
|
||||
|
||||
// Step 2: wait for ConnectRequest
|
||||
Packet? cr = null;
|
||||
while (DateTime.UtcNow < deadline && cr is null)
|
||||
bool connectRequestReceived = false;
|
||||
BorrowedOptionalHeader connectRequest = default;
|
||||
ushort connectRequestIteration = 0;
|
||||
while (DateTime.UtcNow < deadline
|
||||
&& !connectRequestReceived)
|
||||
{
|
||||
PooledInboundDatagram? received =
|
||||
ReceiveBlocking(deadline - DateTime.UtcNow);
|
||||
|
|
@ -812,14 +815,23 @@ public sealed class WorldSession : IDisposable
|
|||
PooledInboundDatagram datagram = received.Value;
|
||||
try
|
||||
{
|
||||
var dec = PacketCodec.TryDecode(
|
||||
datagram.Memory.Span,
|
||||
BorrowedPacketDecodeResult dec =
|
||||
PacketCodec.TryDecodeBorrowed(
|
||||
datagram.Memory,
|
||||
inboundIsaac: null);
|
||||
if (dec.IsOk
|
||||
&& dec.Packet!.Header.HasFlag(
|
||||
&& dec.Packet.Header.HasFlag(
|
||||
PacketHeaderFlags.ConnectRequest))
|
||||
{
|
||||
cr = dec.Packet;
|
||||
connectRequest = dec.Packet.Optional with
|
||||
{
|
||||
RawBytes = ReadOnlyMemory<byte>.Empty,
|
||||
RetransmitRequestBytes =
|
||||
ReadOnlyMemory<byte>.Empty,
|
||||
};
|
||||
connectRequestIteration =
|
||||
dec.Packet.Header.Iteration;
|
||||
connectRequestReceived = true;
|
||||
}
|
||||
}
|
||||
finally
|
||||
|
|
@ -827,10 +839,15 @@ public sealed class WorldSession : IDisposable
|
|||
ReturnInboundDatagram(datagram);
|
||||
}
|
||||
}
|
||||
if (cr is null) { Transition(State.Failed); throw new TimeoutException("ConnectRequest not received"); }
|
||||
if (!connectRequestReceived)
|
||||
{
|
||||
Transition(State.Failed);
|
||||
throw new TimeoutException(
|
||||
"ConnectRequest not received");
|
||||
}
|
||||
|
||||
// Step 3: seed ISAAC, send ConnectResponse to port+1, with 200ms race delay
|
||||
var opt = cr.Optional;
|
||||
BorrowedOptionalHeader opt = connectRequest;
|
||||
|
||||
// Phase G.1: server's initial PortalYearTicks (r12 §1.3) lives
|
||||
// in the ConnectRequest optional section. Publish it to
|
||||
|
|
@ -846,7 +863,7 @@ public sealed class WorldSession : IDisposable
|
|||
// SharedNet::SendOptionalHeader @ 0x00543160 copies this ReceiverData
|
||||
// generation into connection-level control packets, including the
|
||||
// final disconnect. ACE currently emits iteration 1.
|
||||
_sessionIteration = cr.Header.Iteration;
|
||||
_sessionIteration = connectRequestIteration;
|
||||
_transportNegotiated = true;
|
||||
_clientPacketSequence = 2;
|
||||
|
||||
|
|
@ -979,7 +996,7 @@ public sealed class WorldSession : IDisposable
|
|||
{
|
||||
try
|
||||
{
|
||||
ProcessDatagram(datagram.Memory.Span);
|
||||
ProcessDatagram(datagram.Memory);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -1142,7 +1159,7 @@ public sealed class WorldSession : IDisposable
|
|||
try
|
||||
{
|
||||
ProcessDatagram(
|
||||
datagram.Memory.Span,
|
||||
datagram.Memory,
|
||||
opcodesThisCall);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1153,11 +1170,14 @@ public sealed class WorldSession : IDisposable
|
|||
}
|
||||
|
||||
private void ProcessDatagram(
|
||||
ReadOnlySpan<byte> bytes,
|
||||
ReadOnlyMemory<byte> bytes,
|
||||
List<uint>? opcodesOut = null,
|
||||
bool dispatchWorldEvents = true)
|
||||
{
|
||||
var dec = PacketCodec.TryDecode(bytes, _inboundIsaac);
|
||||
BorrowedPacketDecodeResult dec =
|
||||
PacketCodec.TryDecodeBorrowed(
|
||||
bytes,
|
||||
_inboundIsaac);
|
||||
if (!dec.IsOk) return;
|
||||
|
||||
// Retail LinkStatusHolder::OnHeartbeat @ 0x004113D0 updates its
|
||||
|
|
@ -1172,7 +1192,7 @@ public sealed class WorldSession : IDisposable
|
|||
// with "Network Timeout" because it sees no acks coming back —
|
||||
// which surfaces in other clients' views as the player rendering
|
||||
// as a stationary purple haze (loading state).
|
||||
var serverHeader = dec.Packet!.Header;
|
||||
PacketHeader serverHeader = dec.Packet.Header;
|
||||
if (serverHeader.Sequence > 0
|
||||
&& (serverHeader.Flags & PacketHeaderFlags.AckSequence) == 0)
|
||||
{
|
||||
|
|
@ -1184,7 +1204,7 @@ public sealed class WorldSession : IDisposable
|
|||
// periodically — no explicit opcode, just the header flag.
|
||||
if ((serverHeader.Flags & PacketHeaderFlags.TimeSync) != 0)
|
||||
{
|
||||
double t = dec.Packet!.Optional.TimeSync;
|
||||
double t = dec.Packet.Optional.TimeSync;
|
||||
if (t > 0)
|
||||
{
|
||||
LastServerTimeTicks = t;
|
||||
|
|
@ -1192,11 +1212,21 @@ public sealed class WorldSession : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
foreach (var frag in dec.Packet!.Fragments)
|
||||
foreach (BorrowedMessageFragment frag
|
||||
in dec.Packet.Fragments)
|
||||
{
|
||||
var body = _assembler.Ingest(frag, out _);
|
||||
if (body is null || body.Length < 4) continue;
|
||||
uint op = BinaryPrimitives.ReadUInt32LittleEndian(body);
|
||||
if (!_assembler.TryIngest(
|
||||
frag,
|
||||
out ReadOnlyMemory<byte> bodyMemory,
|
||||
out _)
|
||||
|| bodyMemory.Length < 4)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ReadOnlySpan<byte> body = bodyMemory.Span;
|
||||
uint op = BinaryPrimitives.ReadUInt32LittleEndian(
|
||||
body);
|
||||
opcodesOut?.Add(op);
|
||||
|
||||
// Retail waits for the server's opcode-only CharacterLogOff echo
|
||||
|
|
@ -1335,7 +1365,9 @@ public sealed class WorldSession : IDisposable
|
|||
&& !_setStateHexDumped)
|
||||
{
|
||||
_setStateHexDumped = true;
|
||||
var hex = string.Join(" ", body.Take(Math.Min(body.Length, 32))
|
||||
var hex = string.Join(" ", body
|
||||
.Slice(0, Math.Min(body.Length, 32))
|
||||
.ToArray()
|
||||
.Select(b => b.ToString("X2")));
|
||||
Console.WriteLine($"[setstate-hex] body.len={body.Length} first-{Math.Min(body.Length, 32)}-bytes: {hex}");
|
||||
}
|
||||
|
|
@ -1460,7 +1492,8 @@ public sealed class WorldSession : IDisposable
|
|||
// header (guid + sequence + eventType) and dispatch to the
|
||||
// registered handler for that sub-opcode. Unregistered
|
||||
// types get counted for diagnostic overlays.
|
||||
var env = GameEventEnvelope.TryParse(body);
|
||||
var env = GameEventEnvelope.TryParseBorrowed(
|
||||
bodyMemory);
|
||||
if (env is not null) GameEvents.Dispatch(env.Value);
|
||||
}
|
||||
else if (op == 0xEA60u) // AdminEnvirons — server pushes a fog preset or sound cue
|
||||
|
|
@ -1472,7 +1505,7 @@ public sealed class WorldSession : IDisposable
|
|||
if (body.Length >= 8)
|
||||
{
|
||||
uint envType = System.Buffers.Binary.BinaryPrimitives
|
||||
.ReadUInt32LittleEndian(body.AsSpan(4, 4));
|
||||
.ReadUInt32LittleEndian(body.Slice(4, 4));
|
||||
EnvironChanged?.Invoke(envType);
|
||||
}
|
||||
}
|
||||
|
|
@ -1502,7 +1535,7 @@ public sealed class WorldSession : IDisposable
|
|||
if (body.Length >= 6)
|
||||
{
|
||||
ushort sequence = System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(
|
||||
body.AsSpan(4, 2));
|
||||
body.Slice(4, 2));
|
||||
TeleportStarted?.Invoke(sequence);
|
||||
}
|
||||
}
|
||||
|
|
@ -2344,7 +2377,7 @@ public sealed class WorldSession : IDisposable
|
|||
datagram =>
|
||||
{
|
||||
ProcessDatagram(
|
||||
datagram.Memory.Span,
|
||||
datagram.Memory,
|
||||
dispatchWorldEvents: false);
|
||||
return Volatile.Read(ref _characterLogOffConfirmed) != 0;
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue