Skip to content

PERF-12/PERF-20 · Reduce per-datagram allocations in the UDP receive path #428

Description

@LarsLaskowski

Finding: PERF-12 (Medium) + PERF-20 (Low) · Area: F1Server.Service / F1Server.Core
Source: docs/concepts/udp-packet-processing-performance-review.md (branch claude/udp-packet-processing-review-798u7o), sections 2.2 and 2.4

Files involved

  • F1Server.Core/Data/ReceivedPacketData.cs:76-87SetRawData allocates a second array and copies
  • F1Server.Core/Data/ReceivedPacketData.cs:147(PacketTypes)Enum.ToObject(typeof(PacketTypes), packetType + 1) boxes per packet
  • F1Server.Service/TelemetryClient.cs:361-371, 398 — APM receive loop (BeginReceive/EndReceive)
  • Callers of SetRawData: TelemetryClient.cs:371 (UDP: fresh array from EndReceive), TelemetryClient.cs:811 (TCP replay: fresh array via .ToArray()), TelemetryClient.ReceiveDataFromFile:299 (array supplied by external caller), F1ReplayClient/MainWindow.xaml.cs:385

Problem

Every datagram is allocated twice (once by EndReceive, once by the defensive copy in SetRawData) and copied once — at 300-500 packets/s roughly 1 MB/s of avoidable Gen0 garbage, plus per-call IAsyncResult state from the APM pattern. Enum.ToObject adds one boxing allocation per packet.

Step-by-step fix (do steps 1-2; step 3 only with measurement)

  1. Remove the defensive copy: change SetRawData(byte[] rawData) to take ownership of the passed array (_rawData = rawData;). Document the ownership contract in the XML <summary> ("the caller must not modify the array afterwards"). All four current callers pass freshly allocated arrays that they never reuse, so behavior is unchanged — verify each caller and state that in the PR. This also resolves the open question in F-043 · SetRawData allocates a new array per packet and copies, even though the callers (TelemetryClient.cs:811, … #39.
  2. Remove the boxing: replace with a direct cast, e.g. PacketHeader.PacketType = (PacketTypes)(packetType + 1); (identical semantics; optionally validate the value range first).
  3. Optional (measure first): replace the BeginReceive/EndReceive loop with an async Socket.ReceiveFromAsync loop using ArrayPool<byte> buffers. Only do this if allocation profiling after steps 1-2 still shows the receive path as significant. Warning: pooled buffers require explicit lifetime rules because ReceivedPacketData flows into two consumers (processing + file logging) — the buffer may only be returned after both are done. If in doubt, skip step 3 and note it.

Test guidance

Existing parsing tests cover SetRawData consumers. Add one test asserting PacketRawData returns the exact bytes passed in and that PacketHeader is parsed identically to before (compare against a known sample from F1Server.Tests/SampleData).

Acceptance criteria

  • At most one buffer allocation per received datagram on the default path
  • No boxing in the header parse
  • Ownership contract documented on SetRawData; all callers verified and listed in the PR
  • Full rebuild shows 0 Reihitsu (RH####) warnings

Related: #39 (covers the SetRawData copy — close it as resolved by this issue or link the PR to both).

Metadata

Metadata

Assignees

No one assigned

    Labels

    performancePerformance improvements

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions