Skip to content

Networking pipeline optimizations: reduce allocations and lock contention#395

Open
HanielCota wants to merge 6 commits into
hpfxd:masterfrom
HanielCota:optimize/networking-pipeline
Open

Networking pipeline optimizations: reduce allocations and lock contention#395
HanielCota wants to merge 6 commits into
hpfxd:masterfrom
HanielCota:optimize/networking-pipeline

Conversation

@HanielCota
Copy link
Copy Markdown

@HanielCota HanielCota commented May 15, 2026

Summary

This PR contains networking and login pipeline optimizations focused on reducing allocation pressure, eliminating lock contention, and cutting synchronous overhead in hot I/O and player-join paths.


Patch 1 — Reuse input buffer in PacketCompressor (lazy-init, 64KB cap)

File: PacketCompressor.java

PacketCompressor previously allocated a new byte[] for every packet exceeding the compression threshold.

Changes:

  • Added reusable inputBuffer field that is lazily initialized on first compressed packet (idle connections pay zero overhead)
  • Buffer grows only when needed, capped at 64KB (MAX_REUSABLE_SIZE)
  • Packets larger than 64KB allocate a temporary array and are not retained, preventing unbounded per-connection growth

Impact: Reduces GC pressure during chunk sending and large packet bursts; idle connections retain no compressor buffer


Patch 2 — Replace synchronized packet limiter with lock-free AtomicIntervalledCounter

Files: NetworkManager.java, AtomicIntervalledCounter.java (new)

Every inbound packet acquired synchronized(PACKET_LIMIT_LOCK) when packet limiting was enabled, creating unnecessary contention on a per-connection hot path.

Changes:

  • Introduces AtomicIntervalledCounter, a lock-free counter using AtomicLong + dual LongAdder rotation
  • Replaces HashMap<IntervalledCounter> with ConcurrentHashMap<AtomicIntervalledCounter>
  • Removes PACKET_LIMIT_LOCK entirely; stopReadingPackets changed to volatile

Impact: Eliminates monitor enter/exit overhead on every inbound packet when limiter is active


Patch 3 — Reuse PacketDataSerializer instances in Netty handlers

Files: PacketDataSerializer.java, PacketEncoder.java, PacketDecoder.java, PacketCompressor.java, PacketDecompressor.java

Every packet passing through the Netty pipeline triggered new PacketDataSerializer(bytebuf) — a pure wrapper allocation with a single ByteBuf field.

Changes:

  • PacketDataSerializer now uses a ThreadLocal singleton per thread, swapping the underlying ByteBuf via forBuffer(ByteBuf)
  • All 4 Netty handlers (PacketEncoder, PacketDecoder, PacketCompressor, PacketDecompressor) use the reused instance

Impact: Eliminates 1 PacketDataSerializer allocation per packet in encode/decode/compress/decompress paths


Patch 4 — Pool QueuedPacket objects

File: NetworkManager.java

Every packet that couldn't be sent immediately allocated a new QueuedPacket wrapper for the outbound queue.

Changes:

  • Added ArrayDeque<QueuedPacket> pool per NetworkManager
  • obtainQueuedPacket() reuses from pool or allocates only when empty
  • recycleQueuedPacket() returns processed packets to the pool
  • clearPacketQueue() also recycles on connection close

Impact: Eliminates QueuedPacket allocations for the common case of queue-then-send immediately after


Patch 5 — Login pipeline micro-optimizations

Files: PlayerList.java, World.java

The login path had several small but measurable overheads that added up during mass joins.

Changes:

  • Pre-build MC|Brand packet once in PlayerList constructor and reuse for every login (eliminates PacketDataSerializer + Unpooled.buffer() alloc per player)
  • Scoreboard dedup without HashSet in sendScoreboard(): uses a fixed ScoreboardObjective[19] array with reference-equality checks instead of Sets.newHashSet() per login
  • Cache World.getSpawn() result and invalidate only on setSpawn() or world-border resize/center changes (eliminates repeated new BlockPosition + world-border checks)

Impact: Reduces allocation churn and redundant computation during player join


Testing

  • ./panda jar compiles successfully
  • ./panda rb rebuilds patches cleanly
  • All patches apply cleanly on fresh panda setup

Compliance

  • Multi-line changes wrapped with // PandaSpigot start/end comments
  • Oracle Java code style followed
  • Minimal diff size; no public API changes
  • No plugin compatibility impact

Patch files:

  • patches/server/0124-PandaSpigot-Reuse-input-buffer-in-PacketCompressor-t.patch
  • patches/server/0125-PandaSpigot-Replace-synchronized-packet-limiter-with.patch
  • patches/server/0126-PandaSpigot-Reuse-PacketDataSerializer-instances-in-.patch
  • patches/server/0127-PandaSpigot-Pool-QueuedPacket-objects-to-avoid-alloc.patch
  • patches/server/0128-PandaSpigot-Login-optimizations-cache-brand-packet-d.patch

Copilot AI review requested due to automatic review settings May 15, 2026 16:09
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a micro-optimization to the server networking compression path by reusing a heap input buffer in PacketCompressor, aiming to reduce per-packet allocations and GC pressure during high-throughput packet bursts.

Changes:

  • Add a reusable inputBuffer field to avoid allocating a new byte[] for each compressed packet.
  • Grow the reusable buffer only when a packet exceeds the current capacity, and reuse it across encodes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +15 to +17
private final byte[] a = new byte[8192];
+ private byte[] inputBuffer = new byte[8192]; // PandaSpigot - reusable input buffer to avoid allocation per compressed packet
private final Deflater b;
Comment on lines +29 to +35
+ // PandaSpigot start - grow reusable input buffer instead of allocating
+ if (i > this.inputBuffer.length) {
+ this.inputBuffer = new byte[i];
+ }
+ bytebuf.readBytes(this.inputBuffer, 0, i);
+ packetdataserializer.b(i);
+ this.b.setInput(this.inputBuffer, 0, i);
@anzz1
Copy link
Copy Markdown

anzz1 commented May 30, 2026

Did you actually reason, test and profile the patches yourself or is it just, pardon my French, AI talking?

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.

3 participants