Skip to content

perf: port upstream memory budget — queue budgeting, contract stats, pool sizing#264

Merged
full-bars merged 9 commits into
perf/upstream-gopacket-removalfrom
perf/upstream-memory-budget
Jul 17, 2026
Merged

perf: port upstream memory budget — queue budgeting, contract stats, pool sizing#264
full-bars merged 9 commits into
perf/upstream-gopacket-removalfrom
perf/upstream-memory-budget

Conversation

@full-bars

@full-bars full-bars commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Caution

Merge Blockers Resolved: This PR ports upstream memory budget changes which originally contained three critical regressions (an unconditional nil-map panic, an unbounded 100% CPU hot-spin on contract stats, and an 80% silent reduction in sharded message pool capacity). All are fixed in this stack.

Features

  • Upstream Memory Budget Port: Ports the upstream transfer queue memory budget plumbing, contract stats epoch worker, and shared byte budget infrastructure.

Stability

  • Hot-Spin Fix: Initializes the ContractStatsEpoch default to 60s in DefaultContractManagerSettings(). Without this, the uninitialized 0 duration triggered an unbounded hot-spin (time.After(0)) in runContractStats(), permanently pinning a core to 100% CPU.
  • Message Pool Sizing: Implements the ResizeMessagePoolsPerClass wrapper in message_pool.go and updates provider/main.go call sites. Restores the legacy per-class-ceiling pool allocation semantics. Bypasses the upstream division-by-pool-count logic which would have starved the proxy's 5-bucket sharded architecture under load and re-introduced heavy GC pressure.

Bug Fixes

  • Nil-Map Panic Fix: Initializes contractStatsEntries and contractStatsCallbacks in NewContractManager() to prevent an unconditional panic on the very first contract creation.

What's Changed

  • fix: restore per-class pool ceiling sizing by @full-bars in 8a4f47b
  • fix: set ContractStatsEpoch default to 60s to prevent hot-spin by @full-bars in ca87690
  • perf: port upstream memory budget — queue budgeting, contract stats, pool sizing by @full-bars in 0823eca

Full Changelog: perf/upstream-gopacket-removal...perf/upstream-memory-budget

@full-bars
full-bars force-pushed the perf/upstream-memory-budget branch from 0fc397d to 912dfac Compare July 12, 2026 10:52
@full-bars
full-bars force-pushed the perf/upstream-gopacket-removal branch from 82f9179 to f6728b3 Compare July 12, 2026 21:49
@full-bars
full-bars force-pushed the perf/upstream-memory-budget branch 3 times, most recently from 18789ab to 8a4f47b Compare July 13, 2026 00:27
@full-bars
full-bars force-pushed the perf/upstream-gopacket-removal branch 2 times, most recently from 4519357 to 069a964 Compare July 13, 2026 10:39
…2, 3.3)

Covers 7440e8b's fixes:
 - CheckpointContract now releases the client-side stats entry
   immediately (worker never started) or marks it closed for the
   epoch worker to reap (worker started) — the leak from 3.1.
 - ContractStatsEpoch<=0 is normalized to a safe default instead of
   spinning on time.After(0) — 3.3, plus a fire-count check over
   200ms to distinguish a normalized epoch from a hot-spin.
 - An end-to-end two-client test drives 20 messages through a single
   receive-side contract, closes the sequence, and asserts the final
   contract-stats event's UsedByteCount exactly matches the ground
   truth ReceiveContractCloseByteCount from LocalStats — pins 3.2
   (stats previously lagged by one ack, from being updated in
   updateContract's reservation step instead of receiveHead's ack
   step, permanently losing the last message's bytes on close).

All three verified against the unfixed code (reverted 7440e8b's
transfer.go/transfer_contract_manager.go diff locally, confirmed each
test fails with the exact original failure mode, restored). Full
suite (go test . -short -skip TestDohQuery) passes clean.
@full-bars
full-bars force-pushed the perf/upstream-memory-budget branch from e3f6007 to 8821a43 Compare July 13, 2026 10:41
@full-bars full-bars self-assigned this Jul 13, 2026
The previous commit ("prevent contract stats from closing on
checkpoints") guarded closeContractStats behind `if !checkpoint`,
copying the pattern used a few lines above for the unrelated
localStats.ContractOpenByteCounts/ContractOpenKeys bookkeeping. That
guard is correct there — the wire-level contract may still be reused
by a new sequence on checkpoint — but wrong for closeContractStats:
the per-contract stats entry's lifecycle is tied to this sequence's
Run() ending, not to the wire contract's lifecycle, so it must always
release here.

This reintroduced the exact leak TestContractStatsClosedOnCheckpoint
(finding 3.1) was written to catch, and broke
TestContractStatsReceiveSideNotLagging the same way — both now pass.
Per ReceiveSequence.Run's defer, checkpoint is the *common* path a
sequence takes on exit (only the previous, superseded contracts get a
hard CloseContract), so this leaked for most real contract lifecycles.
@full-bars
full-bars merged commit cda883f into perf/upstream-gopacket-removal Jul 17, 2026
@full-bars
full-bars deleted the perf/upstream-memory-budget branch July 17, 2026 01:16
full-bars added a commit that referenced this pull request Jul 17, 2026
* chore: port upstream new infrastructure files (ee7a476 + 83dc999)

Adds 11 new files from upstream connect main (ahead of v2026.7.6-985989570):

New files — no behavioural changes to existing code:
- ip_assoc.go (1135L) — activity-based IP co-association matrix
- ip_block_action.go (570L) — block action overrides for security policy
- memory_budget.go (56L) — process-wide memory budget scaling
- transfer_memory_budget.go (74L) — shared byte budget for sequences
- transfer_contract_stats.go (164L) — per-contract usage event callbacks
- egress.go / egress_net.go / egress_other.go / egress_windows.go (181L) — egress interface binding
- transfer_peer_manager.go (166L) — peer tracking from control messages
- race_on_test.go / race_off_test.go — test helpers for race detection

Supporting additions to existing files:
- protocol/frame.pb.go, protocol/transfer.pb.go — regenerated for new message types
- transfer.go — Peer, NetworkPeer types
- util.go — memoryShedders, AddMemoryShedder, ShedMemory
- ip_remote_multi_client.go — ServerNameLookup interface, EventEpoch field + default
- transfer_contract_manager.go — SignStoredContract, VerifyStoredContract, NetworkEventTimeChangeHmac field, ContractStatsEpoch field, contract stats struct fields

New files compile against the fork's existing interfaces. Test files
that depend on not-yet-ported methods (block actions, packet stats,
IpAssoc wiring) are deferred to later PRs.

* chore: port upstream Peer API — ReceiveFunction type change + pause fix

Changes ReceiveFunction signature from `provideMode protocol.ProvideMode`
to `peer Peer` (upstream 83dc999). Updates all 7 source file signatures,
4 test file lambdas, and connectctl. Adds ProvideMode_Network to the
contract-manager pause filter so network peers don't fall back to stream
contracts when paused.

* perf: port upstream gopacket removal + IpAssoc wiring (ee7a476)

Replaces gopacket-based packet parsing/building with manual binary
encoding on all hot paths in ip.go. Adds packet stats, ipPath caching,
and lock-free multiClientConfig refactoring in ip_remote_multi_client.go.

- Removes github.com/google/gopacket dependency from ip.go
- Custom IPProtocol/UDPPort/TCPPort types + manual IPv4/IPv6/TCP/UDP
  parse and write functions
- New fields: ipAssoc, blockAction state, packetStats on RemoteUserNatMultiClient
- Atomic config pointer replaces separate performanceProfile + localSecurityBypass fields
- Test file updated to use new types

* fix: don't tear down NAT shard on malformed packet, guard empty packets

runShard's bare `return` on parse failure exited the whole loop, firing
defer self.cancel() and killing the shard permanently on a single
malformed TCP/UDP segment from any client (remote DoS). Changed to
continue so only the bad packet is dropped.

Also add the len(ipPacket) == 0 guard in ParseIpPathWithPayload that
upstream's reference port includes, preventing an index-out-of-range
panic on an empty packet.

* fix: TCP checksum scope, option parsing, SynAck padding, pool-return gaps

Five issues found in independent review of the gopacket-removal port:

1. tcpPacket()/SynAck() computed the TCP checksum over the header only,
   excluding payload/options. Every TCP data segment sent to clients
   carried an invalid checksum and would be silently dropped by
   standard client network stacks. Fixed to checksum the full segment,
   matching the UDP path.

2. parseWindowScaleOpts didn't handle TCP option kinds 0 (EOL) and 1
   (NOP), which have no length byte. Since real SYNs almost always
   place a NOP before the Window Scale option, the parser desynced and
   missed WS for nearly all real clients. Fixed to handle both.

3. SynAck() allocated an unpadded options buffer (3 bytes) while
   writeTcpHeader's data-offset field rounds up to a 4-byte boundary
   (24), producing a segment 1 byte shorter than its own header claims.
   Receiving stacks reject this as truncated. Fixed to pad options to
   a 4-byte boundary (zero-fill reads as EOL).

4. tcpSend's RST branch didn't return ipPacket to the message pool on
   an existing-sequence teardown, unlike the sibling no-syn-drop branch.
   Fixed to return it.

5. runShard's ipVersion switch had no default case, leaking the pool
   buffer for any packet with a first nibble outside {4, 6}. Fixed to
   add one, matching the existing per-protocol default branches.

* feat: extract ParseTcpWindowScaleOpts + comprehensive ip packet tests

- Extract the TCP window-scale option parser from TcpSequence.Run()'s
  inline closure to a package-level exported function for testability.
- Add ip_packet_test.go with 33 tests covering:
  - checksumAdd/checksumFinish (RFC 1071)
  - transportChecksum (pseudo-header + segment)
  - TCP checksum scope (payload, options)
  - ConnectionState PureAck/RstAck packet construction
  - parseIpv4/parseIpv6/parseUdpPacket/parseTcpPacket round trips
  - ParseTcpWindowScaleOpts (nil, empty, WS, cap-at-14, NOP, EOL,
    unknown options, multi-NOP)
  - writeIpv4Header/writeIpv6Header/writeTcpHeader round trips
  - flowHash (IPv4/IPv6 consistency, short packets)
  - malformed packet guards (nil, empty, invalid data offset)
  - SynAck data-offset/segment-length consistency
  - UDP checksum non-zero invariant
- Fix TestChecksumAddAndFinish expected value.

* test: extend ip_packet_test.go to 55 wire-format tests

Adds unit-level checksumAdd/checksumFinish coverage, PureAck/RstAck
construction tests, parseIpv4/parseIpv6/parseUdpPacket/parseTcpPacket
round trips, writeIpv4Header/writeIpv6Header/writeTcpHeader round
trips, malformed/too-short input guards, empty-packet guard, and
flow-hash edge cases, on top of the checksum-scope and window-scale
option tests from the prior commit.

Fixed one flaky test in the process: TestWriteIpv4RoundTrip verified
the IPv4 checksum over the full allocated buffer (including 8 bytes
of unrelated UDP-header space past the 20-byte IP header), which only
holds when the message pool happens to hand back a zeroed buffer.
Pool buffers aren't guaranteed zeroed on reuse, so this failed
intermittently depending on prior test pool state. Scoped the
verification to the actual 20-byte IPv4 header, matching what
writeIpv4Header itself checksums. Verified with -shuffle=on across
multiple runs after the fix.

* fix: prevent MTU capacity panics and correct test checksum calculation sequence

* test: add MTU capacity guard regression tests (findings 2.1/2.2)

Covers c4065b4's fix directly: StreamState.DataPackets (UDP) and
ConnectionState.DataPackets (TCP) both now error on mtu <= header
size instead of divide-by-zero / negative-capacity panicking.
Verified against the unfixed code — reproduces the exact
"makeslice: cap out of range" panic from the audit report.

* perf: port upstream memory budget — queue budgeting, contract stats, pool sizing (#264)

* perf: port upstream memory budget — queue budgeting, contract stats, pool sizing

* fix: set ContractStatsEpoch default to 60s to prevent hot-spin

* fix: restore per-class pool ceiling sizing

* fix: contract stats memory leak, CPU loop, and lagging receive stats

* test: add regression tests for contract-stats audit findings (3.1, 3.2, 3.3)

Covers 7440e8b's fixes:
 - CheckpointContract now releases the client-side stats entry
   immediately (worker never started) or marks it closed for the
   epoch worker to reap (worker started) — the leak from 3.1.
 - ContractStatsEpoch<=0 is normalized to a safe default instead of
   spinning on time.After(0) — 3.3, plus a fire-count check over
   200ms to distinguish a normalized epoch from a hot-spin.
 - An end-to-end two-client test drives 20 messages through a single
   receive-side contract, closes the sequence, and asserts the final
   contract-stats event's UsedByteCount exactly matches the ground
   truth ReceiveContractCloseByteCount from LocalStats — pins 3.2
   (stats previously lagged by one ack, from being updated in
   updateContract's reservation step instead of receiveHead's ack
   step, permanently losing the last message's bytes on close).

All three verified against the unfixed code (reverted 7440e8b's
transfer.go/transfer_contract_manager.go diff locally, confirmed each
test fails with the exact original failure mode, restored). Full
suite (go test . -short -skip TestDohQuery) passes clean.

* fix: restore 4 MiB ResendQueueMaxByteCount scaling base

* fix: ReceiveSequence nil map panic on evicted contracts

* fix: prevent contract stats from closing on checkpoints

* fix: closeContractStats must run on checkpoint, not just hard close

The previous commit ("prevent contract stats from closing on
checkpoints") guarded closeContractStats behind `if !checkpoint`,
copying the pattern used a few lines above for the unrelated
localStats.ContractOpenByteCounts/ContractOpenKeys bookkeeping. That
guard is correct there — the wire-level contract may still be reused
by a new sequence on checkpoint — but wrong for closeContractStats:
the per-contract stats entry's lifecycle is tied to this sequence's
Run() ending, not to the wire contract's lifecycle, so it must always
release here.

This reintroduced the exact leak TestContractStatsClosedOnCheckpoint
(finding 3.1) was written to catch, and broke
TestContractStatsReceiveSideNotLagging the same way — both now pass.
Per ReceiveSequence.Run's defer, checkpoint is the *common* path a
sequence takes on exit (only the previous, superseded contracts get a
hard CloseContract), so this leaked for most real contract lifecycles.
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