Skip to content

Reduce per-packet allocations in networking hot path#540

Merged
Reetus merged 1 commit into
ccchangesfrom
perf/networking-hot-path-optimizations
Mar 30, 2026
Merged

Reduce per-packet allocations in networking hot path#540
Reetus merged 1 commit into
ccchangesfrom
perf/networking-hot-path-optimizations

Conversation

@Reetus
Copy link
Copy Markdown
Owner

@Reetus Reetus commented Mar 30, 2026

PacketReader: Replace MemoryStream with direct byte[] + int position. Eliminates MemoryStream allocation per packet and removes temp byte[] allocations in ReadInt16/ReadInt32 (was 10-80 allocs per packet). Add bounds safety matching old MemoryStream behavior. Expand test coverage from 1 to 35 tests.

IncomingPacketHandlers: Cache Version objects, tab separator, and XYComparer as static readonly fields. Call GetItems() once in OnMobileIncoming instead of 3x. Replace LINQ anonymous type chains in OnItemAddedToContainer and OnItemDeleted with simple for loops.

Summary by CodeRabbit

Release Notes

  • Tests

    • Significantly expanded unit test coverage for packet reading operations, including construction, scalar and bulk reads, string parsing, navigation, and data access.
  • Refactor

    • Optimised internal packet handling logic to improve performance and reliability. Streamlined network handlers through code restructuring and helper optimisation.

PacketReader: Replace MemoryStream with direct byte[] + int position.
Eliminates MemoryStream allocation per packet and removes temp byte[]
allocations in ReadInt16/ReadInt32 (was 10-80 allocs per packet).
Add bounds safety matching old MemoryStream behavior. Expand test
coverage from 1 to 35 tests.

IncomingPacketHandlers: Cache Version objects, tab separator, and
XYComparer as static readonly fields. Call GetItems() once in
OnMobileIncoming instead of 3x. Replace LINQ anonymous type chains
in OnItemAddedToContainer and OnItemDeleted with simple for loops.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 30, 2026

Walkthrough

The PR refactors PacketReader to eliminate MemoryStream dependency by reading directly from byte buffers with manual position tracking. Comprehensive unit tests are added to validate PacketReader behaviour across construction, reads, and navigation. IncomingPacketHandlers is optimised by caching version constants and improving container item materialisation efficiency.

Changes

Cohort / File(s) Summary
PacketReader Core Refactoring
ClassicAssist/UO/Data/PacketReader.cs
Replaced stream-based reading with direct byte buffer access via _data and manual _position tracking. All primitive reads (ReadByte, ReadBoolean, ReadSByte, ReadInt16, ReadInt32, etc.) now perform bounds checks against _position/_size and advance position manually. String/Unicode reads refactored to compute available bytes and handle fixed-length/null-terminated variants directly. GetData() now returns a copied slice. Seek() reimplemented for manual position updates.
PacketReader Test Coverage
ClassicAssist.Tests/PacketReaderTests.cs
Added comprehensive unit test suite covering construction behaviour (fixed vs variable length), scalar reads, bulk reads with zero-length cases, string reads (null-terminated, fixed-length with/without truncation, safe filtering), Unicode reads (big-endian/little-endian variants), navigation (Seek with all origins), data exposure (GetData copy semantics), and mixed-type reads from constrained packets.
Handler Optimisations
ClassicAssist/UO/Network/IncomingPacketHandlers.cs
Introduced cached Version constants to centralise client-version comparisons. Added FindLayerBySerial() helper for explicit layer-array iteration in item container handlers. Optimised mobile equipment initialisation by materialising container items once (Item[] equippedItems) and reusing the array for layer setting and Engine.Items addition. Centralised journal property argument parsing with shared tab separator constant.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarises the primary objective of the changeset: reducing allocations in the networking hot path through PacketReader refactoring and IncomingPacketHandlers optimisations.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/networking-hot-path-optimizations

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ClassicAssist/UO/Data/PacketReader.cs (1)

97-109: ⚠️ Potential issue | 🟠 Major

ReadString() is off by one at EOF.

Line 103 requires one spare byte before it will read anything. If the terminator sits in the final byte, it never gets consumed; if the string is unterminated, the last character is dropped outright.

Suggested fix
-            while ( _position + 1 < _size && ( c = _data[_position++] ) != 0 )
+            while ( _position < _size && ( c = _data[_position++] ) != 0 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ClassicAssist/UO/Data/PacketReader.cs` around lines 97 - 109, ReadString
currently uses while (_position + 1 < _size && (c = _data[_position++]) != 0)
which skips the final byte and drops the last char for unterminated strings;
change the loop to run while (_position < _size), read c = _data[_position++],
break if c == 0, otherwise append (char)c to the StringBuilder so the terminator
in the final byte is consumed and the last character of an unterminated string
is preserved; update the ReadString method accordingly.
🧹 Nitpick comments (1)
ClassicAssist.Tests/PacketReaderTests.cs (1)

373-382: Please extend the boundary coverage around Index.

Line 381 only checks the returned string, so the EOF/null-terminator regression in ReadString() still passes, and Lines 477-480 only exercise ReadByte(), so they won't catch truncated ReadInt16/ReadInt32/fixed-width reads drifting Index past Size. A couple of Index assertions here would lock those edges down.

Also applies to: 471-480

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ClassicAssist.Tests/PacketReaderTests.cs` around lines 373 - 382, The test
ReadStringEmptyNullTerminated validates only the returned string but not the
PacketReader cursor, so regressions that mis-handle EOF/null-terminator and
advance Index incorrectly will slip; update this test (and the similar block
around the ReadByte tests at 471-480) to assert the PacketReader.Index (and
optionally PacketReader.Size) after calling PacketReader.ReadString(), and add
small additional tests that call ReadInt16/ReadInt32/fixed-width reads on
truncated buffers to assert Index does not advance past Size (use the
PacketReader constructor and methods ReadString, ReadByte, ReadInt16, ReadInt32
and check the Index property) to lock down boundary behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ClassicAssist/UO/Data/PacketReader.cs`:
- Around line 55-84: Multiple read methods (ReadByteArray, ReadInt16, ReadInt32
and other fixed-width readers) advance _position by the requested width even
when fewer bytes are available, which can leave _position > _size; change the
position update to clamp to Size by using the actual available count or
Math.Min(_position + requestedWidth, _size) so _position never exceeds _size
(e.g. replace "_position += length" with clamped advance for ReadByteArray, and
similarly clamp advances of 2 in ReadInt16 and 4 in ReadInt32 and in any other
fixed-width readers referenced in the comment).
- Around line 13-18: The PacketReader constructor does not validate its inputs
which can leave _data/_size/_position invalid; update the PacketReader(byte[]
data, int size, bool fixedSize) constructor to validate that data is not null
and that size is between 0 and data.Length (inclusive/appropriate), and ensure
the computed _position (fixedSize ? 1 : 3) is <= size; throw
ArgumentNullException for null data and ArgumentOutOfRangeException for invalid
size/position so GetData() and scalar read methods fail fast instead of later.
- Around line 236-251: The Seek method can set _position to a negative value (in
cases in SeekOrigin.Begin, Current, and End), which allows subsequent reads to
index _data with a negative index; modify Seek (the Seek(int offset, SeekOrigin
origin) method) to clamp or reject negative positions by ensuring _position is
never less than zero (e.g., after computing the new _position, if _position < 0
then set it to 0 or throw an ArgumentOutOfRangeException), and also ensure it
still enforces the existing upper bound against _size; update any callers or
comments if you choose to throw instead of clamping.

In `@ClassicAssist/UO/Network/IncomingPacketHandlers.cs`:
- Around line 1497-1531: The code clears the layer slot but leaves the item in
the owner's Equipment collection and also only searches Engine.Mobiles on
deletes (missing PlayerMobile), causing stale state; update the logic around
FindLayerBySerial / SetLayer (the branch handling non-mobile containerSerial and
the Mobile delete branch that uses Engine.Mobiles.SelectEntity and
mobile.SetLayer) to also remove the item from the owner's Equipment list (e.g.,
call the same removal used when unequipping) whenever you clear a layer, and
extend deletion handling to check/handle PlayerMobile (Engine.Player) in
addition to Engine.Mobiles so player-equipped items are cleaned up; ensure the
fix is applied to the other similar blocks mentioned (the other occurrences of
the same pattern) so layer and Equipment stay in sync.

---

Outside diff comments:
In `@ClassicAssist/UO/Data/PacketReader.cs`:
- Around line 97-109: ReadString currently uses while (_position + 1 < _size &&
(c = _data[_position++]) != 0) which skips the final byte and drops the last
char for unterminated strings; change the loop to run while (_position < _size),
read c = _data[_position++], break if c == 0, otherwise append (char)c to the
StringBuilder so the terminator in the final byte is consumed and the last
character of an unterminated string is preserved; update the ReadString method
accordingly.

---

Nitpick comments:
In `@ClassicAssist.Tests/PacketReaderTests.cs`:
- Around line 373-382: The test ReadStringEmptyNullTerminated validates only the
returned string but not the PacketReader cursor, so regressions that mis-handle
EOF/null-terminator and advance Index incorrectly will slip; update this test
(and the similar block around the ReadByte tests at 471-480) to assert the
PacketReader.Index (and optionally PacketReader.Size) after calling
PacketReader.ReadString(), and add small additional tests that call
ReadInt16/ReadInt32/fixed-width reads on truncated buffers to assert Index does
not advance past Size (use the PacketReader constructor and methods ReadString,
ReadByte, ReadInt16, ReadInt32 and check the Index property) to lock down
boundary behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0dbee2bf-10c5-445d-8e2c-0647dfd66be9

📥 Commits

Reviewing files that changed from the base of the PR and between 3e09f07 and 91f2016.

📒 Files selected for processing (3)
  • ClassicAssist.Tests/PacketReaderTests.cs
  • ClassicAssist/UO/Data/PacketReader.cs
  • ClassicAssist/UO/Network/IncomingPacketHandlers.cs

Comment thread ClassicAssist/UO/Data/PacketReader.cs
Comment thread ClassicAssist/UO/Data/PacketReader.cs
Comment thread ClassicAssist/UO/Data/PacketReader.cs
Comment thread ClassicAssist/UO/Network/IncomingPacketHandlers.cs
@Reetus Reetus changed the base branch from master to ccchanges March 30, 2026 23:45
@Reetus Reetus merged commit 75456c3 into ccchanges Mar 30, 2026
2 checks passed
@Reetus Reetus deleted the perf/networking-hot-path-optimizations branch March 30, 2026 23:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant