Skip to content

perf(network): Reduce packet allocations and improve loop logic#2659

Open
Caball009 wants to merge 6 commits into
TheSuperHackers:mainfrom
Caball009:perf_network
Open

perf(network): Reduce packet allocations and improve loop logic#2659
Caball009 wants to merge 6 commits into
TheSuperHackers:mainfrom
Caball009:perf_network

Conversation

@Caball009

@Caball009 Caball009 commented Apr 28, 2026

Copy link
Copy Markdown

This PR makes modest performance improvements in the networking code by reducing the number of dynamic NetPacket allocations and breaking out loops as early as possible.

See commits for cleaner diffs.

TODO:

@Caball009 Caball009 added Minor Severity: Minor < Major < Critical < Blocker Performance Is a performance concern Network Anything related to network, servers labels Apr 28, 2026
@Caball009 Caball009 changed the title perf(network): Improve performancy by reducing packet allocations and improving loop logic perf(network): Improve performance by reducing packet allocations and improving loop logic Apr 28, 2026
@Caball009 Caball009 force-pushed the perf_network branch 2 times, most recently from 7ebd8f1 to a2267a1 Compare April 28, 2026 20:41
@stephanmeesters

Copy link
Copy Markdown

I think it would be nice if we can change the signature of NetPacket from class NetPacket : public MemoryPoolObject to struct NetPacket. The memory pooling right now doesn't really get used, especially with your change? By the looks of it this would mostly need changes around NetPacket::ConstructBigCommandPacketList

@Caball009

Copy link
Copy Markdown
Author

I think it would be nice if we can change the signature of NetPacket from class NetPacket : public MemoryPoolObject to struct NetPacket. The memory pooling right now doesn't really get used, especially with your change? By the looks of it this would mostly need changes around NetPacket::ConstructBigCommandPacketList

Perhaps I can just put the relevant logic from Connection::sendNetCommandMsg in a callback and pass that to NetPacket::ConstructBigCommandPacketList.

@Caball009

Copy link
Copy Markdown
Author

I think it would be nice if we can change the signature of NetPacket from class NetPacket : public MemoryPoolObject to struct NetPacket. The memory pooling right now doesn't really get used, especially with your change? By the looks of it this would mostly need changes around NetPacket::ConstructBigCommandPacketList

I think it's best to do this in a separate pull request because it involves another set of changes.

@Caball009 Caball009 force-pushed the perf_network branch 2 times, most recently from 3fe1916 to d4f75e9 Compare June 2, 2026 20:59
@Caball009 Caball009 marked this pull request as ready for review July 9, 2026 18:59
@greptile-apps

greptile-apps Bot commented Jul 9, 2026

Copy link
Copy Markdown

Greptile Summary

This PR reduces dynamic NetPacket heap allocations across the networking layer by replacing per-call newInstance/deleteInstance pairs with function-scoped static instances (reset via reset() before each use), and accelerates inbound-buffer iteration by breaking out of consumption loops at the first empty slot instead of scanning all MAX_MESSAGES entries.

  • Static NetPacket reuse: Connection::sendNetCommandMsg, Connection::doSend, and ConnectionManager::doRelay each now hold a single static NetPacket* initialized once; reset() replaces per-iteration allocation and is verified safe for sequential single-threaded access across multiple Connection instances.
  • Early-exit break in buffer consumers: ConnectionManager::doRelay, LANAPI::update, and NAT::connectionUpdate all break on the first zero-length m_inBuffer entry, relying on the dense-packing invariant upheld by Transport::doRecv (which fills slots sequentially from index 0, resetting bufferIndex to 0 on each call).
  • CopyTransportMessage extraction: The constructor's transport-copy logic is factored into a dedicated method callable on an already-initialized packet, enabling the static-reuse pattern in doRelay without re-allocating.

Confidence Score: 5/5

The changes are safe to merge; the optimization assumptions hold throughout the networking layer.

All three optimization pillars — static packet reuse, early-exit loop breaks, and CopyTransportMessage extraction — are correctly implemented. The early-exit break relies on m_inBuffer being densely packed, and this invariant is soundly maintained: doRecv resets bufferIndex to 0 on each call and fills sequentially, while every consumer clears slots from index 0 upward before breaking. No data can be orphaned between calls. The reuse of tempref instead of a fresh origref in the oversized-command path of sendNetCommandMsg is safe because addCommand does not take ownership of or modify the ref on failure.

No files require special attention.

Important Files Changed

Filename Overview
Core/GameEngine/Include/GameNetwork/NetPacket.h Constructor signature changed from pointer to const-ref; new CopyTransportMessage() method declared — clean, non-breaking API improvement.
Core/GameEngine/Source/GameNetwork/NetPacket.cpp CopyTransportMessage() extracted from constructor; init() still called first in the constructor before CopyTransportMessage, so field initialization order is preserved.
Core/GameEngine/Source/GameNetwork/Connection.cpp Two per-call NetPacket allocations replaced with function-scoped statics. Reuse of tempref for the oversized-command path is safe because addCommand does not take ownership or modify the ref on failure.
Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp Static NetPacket replaces per-iteration allocation in doRelay; break on empty inBuffer slot is safe given the dense-packing invariant maintained by Transport::doRecv. The spurious deleteInstance(nullptr) at the end of the original doRelay is correctly removed.
Core/GameEngine/Source/GameNetwork/Transport.cpp bufferIndex persists across Read() iterations within a single doRecv() call, avoiding redundant scans from index 0 for each packet. Dense-packing invariant is maintained: bufferIndex resets to 0 each doRecv() invocation, and consumers always clear from slot 0 upward.
Core/GameEngine/Source/GameNetwork/LANAPI.cpp Loop modernized to use size_t and ARRAY_SIZE; break on empty slot added while the !LANbuttonPushed guard is correctly preserved in the loop condition.
Core/GameEngine/Source/GameNetwork/NAT.cpp Loop modernized to use size_t and ARRAY_SIZE; break on empty slot correctly mirrors the dense-packing assumption applied in the other consumers.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Caller
    participant Connection
    participant Transport
    participant ConnectionManager
    participant NetPacket

    Note over Connection: Static NetPacket allocated once
    Caller->>Connection: sendNetCommandMsg(msg, relay)
    Connection->>NetPacket: reset()
    Connection->>NetPacket: addCommand(tempref)
    alt fits in one packet
        NetPacket-->>Connection: true
        Connection->>Connection: addMessage to m_netCommandList
    else too large to fit
        NetPacket-->>Connection: false
        Connection->>NetPacket: ConstructBigCommandPacketList(tempref)
        Connection->>Transport: queueSend per split packet
    end

    Note over Connection: Static NetPacket reused each loop
    Caller->>Connection: doSend()
    loop while commands remain
        Connection->>NetPacket: reset() then addCommand inner loop
        Connection->>Transport: queueSend(data)
    end

    Transport->>Transport: doRecv - fills m_inBuffer densely from index 0
    Note over Transport: bufferIndex persists within each doRecv call

    Caller->>ConnectionManager: doRelay()
    Note over ConnectionManager: Static NetPacket allocated once
    loop "i=0 while m_inBuffer entry length > 0"
        ConnectionManager->>NetPacket: reset() then CopyTransportMessage()
        ConnectionManager->>ConnectionManager: processNetCommand or sendRemoteCommand
        ConnectionManager->>Transport: "clear m_inBuffer entry length = 0"
    end
    Note over ConnectionManager: break at first empty slot
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Caller
    participant Connection
    participant Transport
    participant ConnectionManager
    participant NetPacket

    Note over Connection: Static NetPacket allocated once
    Caller->>Connection: sendNetCommandMsg(msg, relay)
    Connection->>NetPacket: reset()
    Connection->>NetPacket: addCommand(tempref)
    alt fits in one packet
        NetPacket-->>Connection: true
        Connection->>Connection: addMessage to m_netCommandList
    else too large to fit
        NetPacket-->>Connection: false
        Connection->>NetPacket: ConstructBigCommandPacketList(tempref)
        Connection->>Transport: queueSend per split packet
    end

    Note over Connection: Static NetPacket reused each loop
    Caller->>Connection: doSend()
    loop while commands remain
        Connection->>NetPacket: reset() then addCommand inner loop
        Connection->>Transport: queueSend(data)
    end

    Transport->>Transport: doRecv - fills m_inBuffer densely from index 0
    Note over Transport: bufferIndex persists within each doRecv call

    Caller->>ConnectionManager: doRelay()
    Note over ConnectionManager: Static NetPacket allocated once
    loop "i=0 while m_inBuffer entry length > 0"
        ConnectionManager->>NetPacket: reset() then CopyTransportMessage()
        ConnectionManager->>ConnectionManager: processNetCommand or sendRemoteCommand
        ConnectionManager->>Transport: "clear m_inBuffer entry length = 0"
    end
    Note over ConnectionManager: break at first empty slot
Loading

Reviews (2): Last reviewed commit: "Addressed feedback." | Re-trigger Greptile

Comment thread Core/GameEngine/Source/GameNetwork/LANAPI.cpp Outdated
@Caball009 Caball009 changed the title perf(network): Improve performance by reducing packet allocations and improving loop logic perf(network): Reduce packet allocations and improve loop logic Jul 9, 2026

@xezon xezon left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This pull does a number of things.

*/
void ConnectionManager::doRelay() {
// this is done so we don't have to allocate and delete a packet every time we relay a message.
static NetPacket* packet = newInstance(NetPacket);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It would be better if this was a stack object then


void init();
void reset();
void CopyTransportMessage(const TransportMessage& msg);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Or setMessage

CopyTransportMessage(msg);
}

void NetPacket::CopyTransportMessage(const TransportMessage& msg)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Better sort it at the right location

m_addr = msg->addr;
m_port = msg->port;
m_addr = msg.addr;
m_port = msg.port;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is new. Did not do that before. Is that ok?

m_transport->m_inBuffer[i].length = 0;
}
} else {
break;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Alternatively could do early breaks in the loops. But its ok either way.

}
else
{
break;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

So there cannot be non-zero buffer after a zero buffer, yes?

for (Int i = 0; i < MAX_MESSAGES; ++i) {
if (m_transport->m_inBuffer[i].length != 0) {
for (size_t i = 0; i < ARRAY_SIZE(m_transport->m_inBuffer); ++i) {
if (m_transport->m_inBuffer[i].length > 0) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Status quo: Compares with > 0, != 0

Expectation:

> 0, <= 0

or

== 0, != 0

@Caball009

Copy link
Copy Markdown
Author

This pull does a number of things.

I can put the loop logic in another PR and put #2866 in here to replace all memory pooled uses of NetPacket (either with a single dynamic allocation or stack allocation if possible).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Minor Severity: Minor < Major < Critical < Blocker Network Anything related to network, servers Performance Is a performance concern

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants