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:
Erik 2026-07-25 05:58:55 +02:00
parent 7211bb1bf7
commit e928c5dd02
18 changed files with 1329 additions and 66 deletions

View file

@ -0,0 +1,88 @@
# Slice H-c2 — borrowed packet decode
## Result
The production receive path no longer materializes an owned `Packet`,
`PacketHeaderOptional`, `BodyBytes`, fragment list, or single-fragment payload
array for every accepted UDP datagram. It now decodes one borrowed view over
the right-sized pooled datagram:
```text
pooled datagram
-> borrowed packet/header/optional views
-> allocation-free fragment enumeration
-> single-fragment message borrows the same storage
-> synchronous message/event parse and dispatch
-> pooled datagram returned
```
Only a multi-fragment logical message copies its fragment payloads. That copy
is required because those bytes must survive beyond the current datagram while
the remaining fragments arrive.
## Lifetime contract
`BorrowedPacket`, `BorrowedOptionalHeader`, and `BorrowedMessageFragment` are
internal types. Their memory is valid only while the caller retains the
datagram owner. `WorldSession.ProcessDatagram` performs parsing and event
dispatch synchronously before returning that owner.
`GameEventEnvelope.TryParseBorrowed` is likewise internal. Registered event
handlers must parse or copy durable state during `Dispatch`; they must not
retain the envelope or payload. The existing public byte-array parser remains
available for independently owned fixtures and callers.
Multi-fragment assembly copies on first acceptance, not on completion, so
returning either source datagram cannot invalidate the partial message.
Conflicting `Count` or `Queue` values for a reused sequence cannot corrupt an
accepted partial. Duplicate fragments remain idempotent.
## Behavior preserved
- optional-header field order exactly matches `PacketHeaderOptional.Parse`;
- unencrypted and ISAAC checksum verification consume the same bytes and key;
- packet ACK and last-heard placement in `WorldSession` are unchanged;
- fragment grouping remains keyed by message `Sequence`;
- complete messages are delivered in the same fragment-index order;
- game-event handlers remain synchronous on the update/render thread;
- malformed zero-count and out-of-range-index fragments are rejected before
assembly rather than indexing invalid storage.
Named-retail `CNetLayerPacket` remains the packet-size reference. ACE's packet
parser and Holtburger's inbound fragment path are the wire/order
cross-references; this slice changes .NET storage ownership only.
## Deterministic evidence
The differential suite feeds the owned and borrowed decoders identical
datagrams and compares:
- all optional fields in their retail wire order;
- retransmit sequence order;
- login payload coverage;
- one and multiple fragments;
- body, optional, fragment-header, and fragment-payload bytes;
- matching seeded ISAAC checksum consumption;
- every malformed-packet error category touched by this slice.
Fragment tests prove that a single-fragment result aliases its source storage,
while a multi-fragment result survives mutations of both source datagrams.
The warmed single-fragment decode/enumeration unit test measures zero
current-thread allocation over 1,000 iterations.
A Release microbenchmark over 500,000 valid 48-byte, single-fragment packets
measured:
| Decoder | Time | Throughput cost | Allocated |
|---|---:|---:|---:|
| owned | 246.372 ms | 492.7 ns/packet | 200,000,040 bytes |
| borrowed | 28.431 ms | 56.9 ns/packet | 40 bytes |
That is an 8.666x decoder microbenchmark speedup and removes the warmed
per-packet allocation (the reported 40 bytes are the benchmark timer).
## Remaining H-c work
H-c3 removes outbound packet/fragment intermediate arrays by writing the exact
wire framing directly into caller-provided storage. H-c closeout then runs the
connected lifecycle, reconnect, portal, interaction, and soak gates.