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
|
|
@ -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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue