Skip to content

perf: bound idle TCP reuse storage - #6

Open
Joshua Tenner (jtenner) wants to merge 3 commits into
agent/tls-full-gridfrom
agent/tcp-reuse-pool-bounds
Open

perf: bound idle TCP reuse storage#6
Joshua Tenner (jtenner) wants to merge 3 commits into
agent/tls-full-gridfrom
agent/tcp-reuse-pool-bounds

Conversation

@jtenner

@jtenner jtenner commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

Bound lneto TCP's idle listener/outbound reuse state so closing quota-owned resources cannot leave a cache proportional to the configured high-water concurrency.

  • replace MaxListeners- and MaxOutboundStreams-sized idle-cache index arrays with one listener-pool slot and one outbound-buffer slot
  • cap initial listener and stream registry capacity hints at 16 instead of preallocating the full configured listener maximum
  • retain at most one idle listener pool, only when it has no more than 256 slots and no more than 1 MiB of backing storage
  • retain at most one idle outbound stream buffer plus released/reset quota-accounting slot, only when the buffer is no larger than 1 MiB
  • keep each lneto tcp.Conn generation-distinct because the stack may retain its registration pointer after abort; only host-owned bytes/accounting are reused
  • zero listener and outbound receive/transmit bytes before either reuse or release
  • destroy excess concurrent pools and drop oversized storage immediately after close
  • clear and release the remaining bounded cache during namespace teardown
  • preserve distinct listener/stream wrappers, stale-resource isolation, quotas, port leases, policy, nonblocking behavior, and guest ABI

Why

The previous adapter preallocated idle-cache index arrays from the configured maximum counts and appended every closed listener pool and outbound stream buffer. The memory was finite, but after quota release the adapter could continue retaining the full concurrency high-water mark. Listener backing bytes were also reset structurally without explicit zeroing.

The new cache is independent of configured concurrency and has explicit byte/metadata ceilings. Listener and stream registries also grow with actual use from small initial hints. Common reopen/reconnect loops still reuse one cleared allocation and quota-accounting object, while bursts and unusually large configurations return memory to the runtime after deterministic cleanup. The lneto connection object itself remains stream-owned and is never recycled into a new wrapper.

Measurements

Controlled linux/amd64 Go 1.24.4 focused benchmarks retain the existing steady listen/connect allocation counts. Ordinary adapter construction improves from:

  • 20,512 to 20,320 B/op
  • 38 to 36 allocations/op

At the valid maximum-listener configuration, adapter construction improves from:

  • 7,359,715 to 4,214,156 B/op, a 42.7% reduction
  • 37 to 36 allocations/op
  • roughly 787 to 387 microseconds in the focused median run; timing remains informational

Steady outbound connect/close allocation also improves from 1,128 to 936 B/op (17.0%) with the same four allocations, while the focused median improves by roughly 3%. This comes from detaching the reusable quota charge from each generation-distinct stream wrapper.

Together these changes remove eager reuse-index allocations and the listener registry allocation proportional to the configured maximum. Close-path timing is treated as informational because listener storage now receives an intentional bounded clear before reuse/release.

Validation

Passed locally and in hosted run 30202877938 on head 8dd761f:

  • go test -shuffle=on -count=1 ./...
  • focused race tests for lneto TCP, aggregate lneto, public TCP, TLS, and lneto TLS
  • go vet ./...
  • scripts/check-source-boundaries.sh
  • TinyGo tests for internal/backend/lneto/tcp and public tcp
  • focused benchmark repeats for adapter creation and listen/connect close loops
  • clean no-conflict merge over draft DNS fallback PR feat: add bounded DNS-over-TCP fallback #4 plus focused TCP/DNS/TLS integration tests
  • hosted test/shuffle/vet/source-boundary, race, standard-Go TLS signoff, checkptr/386, and all 123 TinyGo-supported packages; the TinyGo matrix completed in 48m19s

Scope

This is stacked directly on draft PR #3 (agent/tls-full-grid) and is independent of the DNS fallback and TLS allocation-hardening siblings. It changes no capability, import, policy, quota, port, or guest ABI contract. Keep this PR as a draft while its base is unmerged.

@jtenner

jtenner commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author

Codex (@codex) review

@jtenner
Joshua Tenner (jtenner) marked this pull request as ready for review July 26, 2026 13:56

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8dd761f02d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".

if n.freeListenerPool.slots == nil && len(listener.pool.slots) <= maxIdleTCPListenerSlots && len(listener.pool.storage) <= maxIdleTCPReuseStorageBytes {
n.freeListenerPool = listener.pool
} else {
listener.pool.destroyLocked()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid clearing rejected listener pools twice

When a listener pool cannot be cached because another idle pool exists or it exceeds the slot/storage ceiling, recycleListenerLocked has already called releaseLocked, whose new clear(p.storage) wipes the entire backing allocation. Calling destroyLocked here invokes releaseLocked again, so every rejected pool is cleared twice; with valid listener pools reaching 256 MiB and bursts rejecting all but one pool, close can synchronously write hundreds of extra MiB. Drop the already-released pool without invoking the clearing path again.

Useful? React with 👍 / 👎.

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